If you’ve worked on a Django project past a certain size, you know the feeling: you’re staring at a URL, and you genuinely don’t know — without opening three files and reading branch logic by hand — which template will actually render when a request hits it. Maybe it’s a DetailView relying on naming conventions. Maybe it’s a function view with a try/except that renders three different templates depending on what goes wrong. Maybe someone added a redirect six months ago and the “template” is actually just a bounce to another URL.
PyCharm has a version of this, but it’s locked inside the IDE. I couldn’t find anything else — so I built django-uvtmap, a management command that maps every URL in your project to its view, and every view to the templates it can render, including the conditions under which each one fires.
pip install django-uvtmap
Add "uvtmap" to INSTALLED_APPS, then:
python manage.py uvtmap --html map.html
This is the command that matters — it opens an interactive, self-contained HTML page. No server, no build step. Just three columns — URLs, views, templates — wired together, with hovering tracing a full chain end to end and clicking pinning a node open for inspection.
What it actually does
- Full URL → view → template mapping, for function-based and class-based views alike
- Render conditions — the tool walks the AST upward from every
render()call and
reconstructs the branch path that leads to it:if/elif/else(correctly negated
depending on which branch you’re in),try/except,match/case, loops. So instead
of just “this view rendersdashboard.html,” you get “rendersdash_compact.htmlif
mode == 'compact',dash_full.htmlotherwise,dash_default.htmlonKeyError.” - Redirect tracking —
redirect(),reverse(),reverse_lazy(), and literal-path
HttpResponseRedirect()calls are followed back to their target URL, so a view that
redirects instead of rendering still shows up as a real edge in the map, not a dead end. - Missing-template detection — every statically-resolvable template name is checked
against Django’s actual configured template loaders. If it doesn’t exist, the map
flags it. This accidentally makes the tool double as a linter — I’ve caught stale
template references in my own project just by running it. - CBV support — explicit
template_name, the generic-view default naming
convention (DetailViewonPostcorrectly resolves toblog/post_detail.html
even with nothing set explicitly), andrender()calls inside a class’s own methods. --appand--list-apps— scope the map to a single app, project or third-party
(yes, it’ll mapdjango-allauth’s URLs if you want to see them), or list every app
with URL-routed views before committing to a full run.--jsonoutput for scripting — It could be used in CI to fail a build if any template
comes back"exists": false.
What it deliberately doesn’t do
No requests are made and no views are executed — everything is static analysis. That means a few honest limitations:
- Implicit fallthrough (
if x: return render(a)followed byreturn render(b)) shows
b as “always rendered” instead of “if not x” — that needs real control-flow
analysis, not just an AST walk. - Template names built at runtime (an f-string, a variable) can’t be resolved — the
tool shows you the expression instead and marks it dynamic. get_template_names()overrides on CBVs aren’t traced yet.
These are documented in the README, and I’d rather ship an honest v0.1.0 than an
overclaimed one.
Try it
pip install django-uvtmap
- PyPI: django-uvtmap · PyPI
It’s a fresh release — genuinely interested in bug reports, edge cases I haven’t hit
in my own projects, and feature requests. If your project has a URL pattern this
doesn’t handle correctly, that’s exactly the kind of thing I want to know about.
Built in collaboration with Claude (Anthropic) — concept, prototype, feature direction, and every design call along the way are mine; a lot of the implementation was AI-assisted. Said plainly because I’d rather be upfront about it than have someone read the code and wonder.
