How do you write and register a custom path converter in Django, and what does raising ValueError in its to_python() or to_url() do?
answer
- three members, no base class
- register before the route compiles
- ValueError means no match
- one direction for resolving, one for reversing
- duplicate names since 6.0
basics
~20 sWrite a class with a regex attribute, to_python() and to_url(), then call django.urls.register_converter(cls, 'name') before any path() uses name:.... ValueError in to_python() makes that pattern not match; in to_url() it makes reverse() skip the pattern.
solid answer
~40 sA converter is a plain class with a `regex` string, `to_python(value)` that turns the matched text into the view's argument, and `to_url(value)` that turns a Python value back into URL text. Register it with `register_converter(IsoDateConverter, 'isodate')` in the URLconf before the `path()` calls that use `<isodate:day>`, because routes compile when `path()` runs and an unknown name raises `ImproperlyConfigured`. A `ValueError` from `to_python()` means no match: resolution tries the next pattern and ends in a 404 if none fits. A `ValueError` from `to_url()` makes `reverse()` skip the pattern, ending in `NoReverseMatch` if no other candidate fits. Other exceptions propagate. Django creates one shared instance per registration, and since 6.0 registering an existing name, built-ins included, raises `ValueError`.
code
python · 10 linesimport datetime
from django.urls import reverse
# With path('events/on/<isodate:day>/', ..., name='events-on-day') registered:
reverse('events-on-day', kwargs={'day': datetime.date(2026, 10, 3)})
# '/events/on/2026-10-03/'
reverse('events-on-day', kwargs={'day': 'tomorrow'})
# to_url() raises ValueError -> NoReverseMatchgo deeper
Recall the three members a converter needs and that register_converter() gives it the name used inside angle brackets.
Explain the two ValueError paths, 404 when resolving and NoReverseMatch when reversing, and why registration must run before the routes that use it compile.
Keep converters cheap, stateless and I/O-free, avoid cross-app name collisions, and know the 6.0 change that turned silent overrides into errors.
Decide which URL value types deserve a shared converter library so formatting and validation live in one place across many apps.
## The converter protocol A custom **path converter** is any class with three members; no base class is required: - `regex` - a class attribute string with the text the placeholder accepts inside a route; - `to_python(self, value)` - turns the matched string into the value the view receives; - `to_url(self, value)` - turns a Python value back into URL text when a URL is built from the route's name. The regex runs first. `to_python()` only ever sees text that already matched it, which lets it do the semantic check a regex cannot, such as rejecting 30 February. ## Example: event listings by date An event-ticketing site wants `/events/on/2026-10-03/` to list that day's events, with the view receiving a real `datetime.date`. ```python # events/converters.py import datetime class IsoDateConverter: regex = '[0-9]{4}-[0-9]{2}-[0-9]{2}' def to_python(self, value): return datetime.date.fromisoformat(value) # ValueError for 2026-02-30 def to_url(self, value): if not isinstance(value, datetime.date): raise ValueError('expected a date') return value.isoformat() ``` ```python # events/urls.py from django.urls import path, register_converter from . import converters, views register_converter(converters.IsoDateConverter, 'isodate') urlpatterns = [ path('events/on/<isodate:day>/', views.events_on_day, name='events-on-day'), ] ``` `/events/on/2026-10-03/` calls `events_on_day(request, day=datetime.date(2026, 10, 3))`. `/events/on/2026-02-30/` passes the regex, but `fromisoformat()` raises `ValueError`, so the route does not match. ## What ValueError means in each direction | Raised in | Effect | If no other pattern fits | |---|---|---| | `to_python()` | the pattern does not match; resolution moves to the next entry | the request gets a 404 | | `to_url()` | the pattern is skipped when building a URL | `reverse()` raises `NoReverseMatch` | `ValueError` is the **only** exception Django treats as "no match". Anything else raised inside `to_python()`, such as `KeyError` or a database error, escapes URL resolution and becomes a server error. ## Registration rules 1. **Register before use.** Django compiles a route's regex when `path()` is called. If `<isodate:day>` is compiled before `register_converter()` has run, Django raises `ImproperlyConfigured` saying the route uses an invalid converter. 2. **Register once per name.** Since Django 6.0, registering a name that already exists, whether a built-in like `int` or your own name registered from a second module, raises `ValueError`. Django 5.1 deprecated overriding; older versions silently replaced the converter. 3. **Registration is process-wide.** Once registered, every URLconf in the project can use the name, so pick names that will not collide with another app's. 4. **One shared instance.** `register_converter()` instantiates the class once and reuses that object for every request and thread, so a converter must not keep per-request state. ## When a custom converter is the right tool - The same constraint appears in several routes, and a regex copied into each `re_path()` would drift. - The view should receive a typed value, such as a `date` or an enum member, instead of parsing text itself. - URLs are built from Python values, so formatting belongs in one `to_url()`. Keep `to_python()` cheap and free of database queries or other I/O: it runs during resolution for every request whose text fits its regex, before the view has a chance to handle errors. ## Testing a converter A converter is small enough to test without a server, and three kinds of test cover it: 1. **Unit tests on the class.** Call `IsoDateConverter().to_python('2026-10-03')` and assert a `date`; assert that `'2026-02-30'` raises `ValueError`; assert that `to_url()` rejects a string. 2. **Resolution tests.** Request `/events/on/2026-02-30/` with Django's test client and assert a 404, which proves the `ValueError` is treated as no match rather than an error. 3. **Round-trip tests.** Build a URL from the route name with a `date`, resolve it again, and assert that the view receives an equal `date`. | Check | What it proves | |---|---| | `to_python()` returns the typed value | the view gets a `date`, not text | | invalid text gives 404 | `ValueError` means no match | | round trip is stable | `to_python()` and `to_url()` agree | The round trip matters most: a converter whose two methods disagree produces links that do not resolve.
- Where should register_converter() be called in a Django project?At the top of the URLconf module that first uses the converter, or in a module that URLconf imports, so it runs before those `path()` calls compile. Call it exactly once per name: registering the same name from two apps' URLconfs raises `ValueError` in Django 6.x.
- Can a Django path converter raise Http404 from to_python() instead of ValueError?It would produce a 404, but it bypasses the resolver's no-match handling, so later patterns never get a chance and the converter decides the response on its own. `ValueError` is the documented contract; leave 404 decisions about missing objects to the view.
saying these in an interview costs you the question
- A custom converter must subclass a Django base converter class.
- Any exception raised in to_python() is treated as no match.
- register_converter() can be called after urlpatterns and still apply.
- In Django 6.x, register_converter('int') simply replaces the built-in int converter.
- Django creates a fresh converter instance for every request.