kyro
Symfony-style attribute routing for Django: declare routes with decorators directly on
your views and controllers instead of maintaining a long urls.py.
Status: pre-alpha, under active development.
Installation
pip install kyro-routing # or: uv add kyro-routing
The package is imported as kyro. Add "kyro" to INSTALLED_APPS for the management
commands and the system checks.
Quick start
# blog/views.py
from django.http import HttpResponse
from kyro import route
from blog.models import Post
@route("/hello/{name}", name="hello")
def hello(request, name):
return HttpResponse(f"Hello {name}")
@route("/blog", name="blog_")
class BlogController:
@route("", name="index", methods="GET")
def index(self, request): ...
@route("", name="create", methods="POST")
def create(self, request): ...
@route(r"/page/{page<\d+>?1}", name="list") # before /{slug}, which would catch /page
def list(self, request, page: int): ...
@route("/{slug}", name="show")
def show(self, request, post: Post): # fetched by slug, 404 if missing
...
# project/urls.py
import kyro
urlpatterns = kyro.discover() # imports the `views` module of every installed app
Routes are plain Django URL patterns: reverse("blog_show", kwargs={"slug": "hello"}) and
{% url "blog_index" %} work as usual, and so do include() and namespaces.
Paths
Both syntaxes are accepted, even in the same path:
| Syntax | Meaning |
|---|---|
<name>, <int:id> |
Django parameter and converter |
{name} |
parameter (anything but /) |
{id<\d+>} |
parameter with a regex requirement |
{page?1} |
optional parameter with default "1" |
{page?} |
optional parameter with default None |
Trailing parameters with a default (inline or in defaults=) are optional:
/blog/{page?1} matches /blog and /blog/2.
@route arguments
| Argument | |
|---|---|
path |
the path, relative to the class prefix if any |
name |
route name, appended to the class name prefix; generated when omitted |
methods |
"GET" or ["GET", "POST"]; others get a 405. GET also allows HEAD |
requirements |
{"id": r"\d+"} |
defaults |
defaults of optional parameters and extra kwargs passed to the view |
priority |
higher priority routes are matched first; otherwise declaration order |
Several routes may share a path with different methods: kyro dispatches each request to
the right view. The path is exempt from CSRF checks (csrf_exempt) or from
LoginRequiredMiddleware (login_not_required) only when all its views are. @route can
be stacked to give a view several routes.
What can be routed
- Functions, sync or async. Put
@routeabove other decorators, or below decorators that usefunctools.wraps. A route hidden by a decorator that does not is refused (ImproperlyConfigured): it would serve the view without that decorator. - Methods of any class: the class is instantiated for each request. A
@routeon the class is a prefix (path, name, requirements, defaults, methods) for its methods. Classes defined inside a function must be decorated (@route()for no prefix). - A class with
__call__and no routed method, or a DjangoView: the class route is the route of the class itself. The methods of a DjangoViewcannot be routed: kyro would call them withoutView.dispatch(), skipping mixins, authentication and permissions.
Django REST Framework
Put @route on APIView and generic view classes, and let them handle the HTTP methods
(no methods=): authentication, permissions, CSRF, error responses (400 to 405) and
schema generation work as usual.
from rest_framework.generics import RetrieveUpdateAPIView
@route("/members/<uuid:pk>", name="member")
class MemberDetail(RetrieveUpdateAPIView):
queryset = Member.objects.all()
serializer_class = MemberSerializer
- With
methods=, kyro answers the other methods itself, with a plain Django 405, and schema generators still list them. @routemay be put above@api_view, not below it (@api_viewdoes not usefunctools.wraps).- The methods of an
APIViewcannot be routed, and viewsets are not supported: route one view class per path. - kyro converts no parameter for view classes: use Django converters (
<uuid:pk>).
Parameter conversion
Annotated view arguments are converted from the URL:
-
int,float,Decimal,bool,UUID,date,datetime,Enum: an invalid value is a 404. -
Django models, fetched like Symfony's entity resolver:
{post}forpost: Post: lookup by primary key;{slug}forpost: Post: lookup by fields named like URL parameters (the parameter is only passed to the view if its signature accepts it);post: Annotated[Post, MapModel(slug="post_slug")]: explicit mapping.
A missing object is a 404, or
Nonewhen the annotation is optional (Post | None).
Debugging routes
With "kyro" in INSTALLED_APPS, manage.py debug_router lists every route of the
project, kyro or not, in matching order:
$ python manage.py debug_router
-------------- ---------- ------------------
Name Method Path
-------------- ---------- ------------------
blog_index GET|HEAD /blog
blog_create POST /blog
blog_show ANY /blog/{slug}
-------------- ---------- ------------------
debug_router blog_showshows one route in detail: Django routes, requirements, defaults, priority, view and the file and line where it is declared. A partial name shows the detail when it matches one route, the list of matches otherwise.--show-viewsadds the view of each route to the list,--format jsonoutputs JSON and--urlconfinspects another URLconf thanROOT_URLCONF.
manage.py router_match tells which route serves a request, method dispatch included:
$ python manage.py router_match /blog --method POST
------------- ---------- ------- --------------------
Name Method Path Result
------------- ---------- ------- --------------------
blog_index GET|HEAD /blog method not allowed
blog_create POST /blog match
------------- ---------- ------- --------------------
Route 'blog_create' matches.
It then shows the route in detail with the kwargs its view receives. Routes matching the
path but not the method, or hidden by an earlier route (shadowed), are listed; -v 2
lists every route. The command fails when no route serves the request (404 or 405).
--format json and --urlconf work as for debug_router.
System checks
With "kyro" in INSTALLED_APPS, manage.py check (and runserver) warns about:
| Id | Problem |
|---|---|
kyro.W001 |
a route without parameters is served by an earlier route, for all or some of its methods (/blog/{slug} declared before /blog/new) |
kyro.W002 |
a route name is shared with other routes, so reverse() only returns one of them |
kyro.W003 |
routes declared with @route are not in the URLconf (kyro.discover() forgotten?) |
kyro.W004 |
a parameter has a default but is not trailing, so it cannot be omitted |
Silence one with SILENCED_SYSTEM_CHECKS = ["kyro.W003"].
Settings
KYRO_DISCOVER(default["views"]): app submodules imported bykyro.discover().
kyro.discover(["blog"]) imports the given modules (for a package, its KYRO_DISCOVER
submodules too) and returns only their routes, to mount them under a prefix:
path("blog/", include(kyro.discover(["blog"]))).
Development
uv sync # create .venv and install dependencies
uv run pytest # run the test suite
uv run ruff check . # lint
uv run ruff format . # format
uv run python -m mypy # type check (strict)
Releasing
- Set the version in
src/kyro/__init__.py(__version__), and inCHANGELOG.mdrename[Unreleased]to[x.y.z] - YYYY-MM-DDand update the links at the bottom. - Optionally, try the release out on TestPyPI: Actions → Release → Run workflow.
- Commit, tag and push:
git tag vx.y.z && git push origin main vx.y.z. TheReleaseworkflow publishes to TestPyPI, then PyPI.
Metadata
Release files for kyro-routing 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| kyro_routing-0.1.1.tar.gz | 73.0 kB | With provenance | Details for kyro_routing-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| kyro_routing-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for kyro_routing-0.1.1-py3-none-any.whl |
Total release size: 101.2 kB
Release files / kyro_routing-0.1.1.tar.gz
| Download URL | kyro_routing-0.1.1.tar.gz |
|---|---|
| Size | 73.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8436ac2dbb56a6f781f0121417f35a79eaa540d4031c361ff79d6977eed37cde
|
|
BLAKE2b-256 checksum How to use checksums |
3743481fce9abc89235271e29ef38b0813a435b733a0aa59373f68e7d08de0a6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logRelease files / kyro_routing-0.1.1-py3-none-any.whl
| Download URL | kyro_routing-0.1.1-py3-none-any.whl |
|---|---|
| Size | 28.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
98138c0cffda9b6380b46e1fc70cf1f7b3c596dd12246cfad49193a8d6edfa3b
|
|
BLAKE2b-256 checksum How to use checksums |
7ecde1fa195da9e52c7419f9050a470a27fc18a4c98637d03fd79f34f3bbce21
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logSecurity for kyro-routing 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for kyro_routing-0.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.