Reinout van Rees’ weblog¶
Djangocon EU: lightning talks day 3¶
2026-04-17
(One of my summaries of the 2026 Djangocon EU in Athens).
Announcement - Carlton Gibson¶
They’ve been working on improving the technical governance of Django. They’d like to get feedback. There’s a blog post about it.
Oh, and look at the “30% off PyCharm” button on the django website, that raises quite a lot of funds for Django. PyCharm’s sponsoring is a very sizeable financial part of Django, thanks!
Even more table partitioning with Django, Postgres and UUIDs - Tim Bell¶
(See his earlier talk on partitioning).
UUID is 128-bits, usually displayed as hex strings. It starts with the unix timestamp, followed by several random fields (in version 7). In version 8, you have more flexibility. You can customize it to put a specific value (an id of a related field in their case) in the first field.
Partitioning per UUID (they used it as their ID) then effectively also partitions on the related field.
Speeding up Django startup times with lazy imports - Anze Pecar¶
Imports in Python can be slow. Luckily, python has something build-in to check it, the “importtime” flag:
python -X importtime manage.py check
He worked around the packages he found by importing the package inside the functions where he used them. It worked, but it was ugly.
Look at things like post_worker_init in gunicorn, you can use that to pre-load the
offending modules.
You can also wait for python 3.15. PEP810: explicit lazy imports!
PyLadies Seoul: rebooting a community for women in tech scenes - Hwayoung Cha¶
At Pycon Korea 2023 there were only three woman in attendance. So: time to re-start Pyladies Seoul! And with success. One of the new attendees is now a CTO of a company (and also a PyLadies volunteer herself).
They’ll also start a Django workshop soon.
Join your local PyLadies chapter!
What I learned during learning to solve rubic cube - Venelin Stoykov¶
He learned solving a Rubic cube in about two weeks.
We can learn new things more easily by association with things we already know. We need to practice a lot. Repeat, repeat: that way we tell our brain that we need to remember it.
“Thinking slow and fast” is a book he recommends.
AI is like the fast thinking. Fast is also a bit sloppy and often a bit wrong.
If we really want to understand something, it takes time and work.
Why volunteering and contributing to communities is important - Alex Gómez¶
Get involved! Volunteer! Do some work! Volunteers are necessary.
Volunteering is a lot of work, but it is worth it.
Djangofmt, a Django template formatter written in rust - Thibaut Decombe¶
Djangofmt, a fast, html aware, django template formatter, written in Rust.
https://github.com/UnknownPlatypus/djangofmt
You can run it as a pre-commit hook.
Unrelated photo explanation: a cat I encountered in Athens in the morning near the hotel.
Djangocon EU: supply chain attacks on Python projects - Mateusz Bełczowski¶
2026-04-17
(One of my summaries of the 2026 Djangocon EU in Athens).
Full title: what’s in your dependencies? Supply chain attacks on Python projects.
How supply chain attacks work: attackers don’t attack your code directly, they target
something you trust. A typical Django project has lots of dependencies. Direct
dependencies and “transitive dependencies”, dependencies of our dependencies. If you
depend on requests, requests itself will grab certify and urllib3.
Possible package attacks:
Inject malicious code directly into the repo.
Create malicious package. Typosquatting (abusing typos), slopsquatting (abusing typos made by LLMs). “Brandjacking”: quickly after
deepseekbecame popular, adeepseekaipackage was published that stole credentials.Compromise existing package. Credential stealing, CI/CD exploits.
What attackers typically do with access is to steal credentials. Environment variables,
cloud keys (AWS_xyz), pypi tokens, ssh private keys, database URLs, saved passwords.
Example: num2words was hacked in July 2025. Phishing leading to maintainer credentials
theft. Fake login page at pypj.org instead of pypi.org. Then they uploaded faulty
releases with the captured credentials. Credentials weren’t rotated, so a second attack
happened a few days later. This malware targeted .pypirc files, leading to more
compromises.
How can we defend from this kind of attacks? Depends on the kind of attack. When publishing via GitHub actions, use “trusted publishing”, in that case there are no credentials to steal.
Another example: LiteLLM was compromised via trivy, a security scanner that itself was compromised… It in turn collected environment variables, secrets and ssh keys, bundled it all in a tarball and posted it to some legitimate-looking domain.
Some myths:
“Lockfiles protect us”. No, they only prevent accidental upgrades, not when adding a package for the first time.
“Just don’t install suspicious packages”. Lots is installed via transitive dependencies.
“We run everything in Docker so we’re safe”. It limits the blast radius, but credentials and environment variables are still at risk.
“We can fully prevent attacks”.
Some tips:
Use dependency cooldowns.
uvhas “exclude-newer”,piphas “uploaded-prior-to”. Don’t be the first to install a fresh release, as most malicious packages are discovered within hours or days.Pin versions and verify hashes.
Pypi is getting better:
Trusted publishing.
Project quarantine.
Attestations: cryptographic tools to verify the source.
Typosquatting protection.
AI has risks:
Slopsquatting. Hallucinated package names that get exploited.
Prompt injection via github issues.
Agents often “just” pip-install things directly.
Note: AI can also be used to detect malware! A small project started after the LiteLLM compromise managed to detect a dangerous different compromise almost the moment it was published. Nice!
Unrelated photo explanation: a cat I encountered in Athens on an evening stroll in the neighbourhood behind the hotel. It was a cat on the hunt: relevant to the topic of this talk :-)
Djangocon EU: Django templates on the frontend? - Christophe Henry¶
2026-04-17
(One of my summaries of the 2026 Djangocon EU in Athens).
It all started with formsets: you generate a new form based on other forms. You can use it to create pretty fancy forms. But your designer can get quite creative. And you might have variable forms that have to react to user input.
A common solution is to use htmx, but that means server requests all the time. And some users have really bad connections. Regular requests aren’t handy in that scenario.
He looked at django-rusty-templates: Django’s template engine implemented in Rust. It had a template parser that he could re-use. With OXC (javascript oxidation compiler) he converted that to javascript.
That way, he could offload much of the django form creation handling to the frontend, including reacting to user input and showing alerts.
The work-in-progress project is called django-template-transpiler: https://github.com/christophehenry/django-template-transpiler . Don’t use it for production.
Unrelated photo explanation: a cat I encountered in Athens on an evening stroll in the neighbourhood behind the hotel.
Djangocon EU: How Django is helping to build the biggest X-ray observatory to date - Loes Crama¶
2026-04-17
(One of my summaries of the 2026 Djangocon EU in Athens).
She works at Cosine, they develop measurement systems and space instrumentation. They work for the space industry (ESA, NASA, etc).
They’re now working on “high-energy optics”, the NewAthena x-ray observatory, the biggest one to date. NewAthena: NEW Advanced Telescope for High-ENergy Astrophysics. Planned launch is in 2037 on Ariane 6.4.
Talking about rocket science is cool, but where’s the software? Within the company, software is an internal service, supporting scientists. Handling data in many ways: visualization, analysis, processing, management. Django plays a big role in all of this.
When you build something with Django in a scientific context, you really need to understand the data. Workflows must be flexible. R&D and production often don’t need to be strictly separated. Multiple datastores for various purposes (like an extra MongoDB, for instance) is often handy.
Their application consists of:
SXRO (silicon x-ray optics) database.
A Mysql database.
Django.
The goal is to track all the components that go into the observatory. Status and quality. Configuration and geometry. Component relationships. Inspections. History of all the components.
The default Django admin is their primary method of using the application. Often, it is said that the admin is not not not intended for end users. But they’re using it anyway. It is an internal tool for technical people. Most of them have a PhD: they can handle such an interface. They’ve been using it for years.
There are some third party packages:
django-simple-history: easy history.
djangoql: advanced queries for the search bar.
django-admin-rangefilter, django-admin-list-filter-dropdown, django-admin-numeric-filter: little tools to tweak the filters on the right hand side.
There are some separate forms, mostly for actions performed in the lab or cleanroom. For instance a form where you can use an ipad to indicate defects in one of the components by just drawing on the picture of the component.
There’s also a REST API. Other software and data tools can use it to integrate with Django:
Observability tools (prometheus/grafana).
JupyterHub.
Stacking robots.
They use: djangorestframework, drf-spectacular (API docs), django-filter (filtering via GET parameters).
Django is their software backbone. A general-purpose framework that’s well suited for a scientific context.
Unrelated photo explanation: a cat I encountered in Athens on an evening stroll in the neighbourhood behind the hotel.
Djangocon EU: where did it all BEGIN;? Django’s transaction management - Charlie Denton & Sam Searles-Bryant¶
2026-04-17
(One of my summaries of the 2026 Djangocon EU in Athens).
Transactions are an SQL concept. You “begin” a transaction, do a bunch of statements and at the end “commit” it. Only then does it actually show up in the database for other connections:
BEGIN;
INSERT ...;
UPDATE ...;
INSERT ...;
COMMIT;
If something goes wrong, you can abort it with “rollback”:
BEGIN;
INSERT ...;
UPDATE ...;
INSERT ...;
ROLLBACK;
The SQL default is to run in “autocommit” mode, so everything always updates the database immediately.
The history of Django’s transaction management:
<0.95 emulated sql’s autocommit by executing
COMMIT;after every save.0.95 introduced
with commit_on_success():, which would give you a context manager, at the end everything would be committed unless an error occurred. It did have a corner case: you couldn’t really nestcommit_on_success.1.6 introduced
with atomic():, mostly likecommit_on_success, but it introducedSAVEPOINT, allowing you to rollback a nested.atomic()to the savepoint that’s created when you enter a nested ‘atomic’ block.3.2 introduced
with atomic(durable=True):. If you had a nested atomic, a transaction you thought was committed might be rolled back by another ‘atomic’ wrapping the code. When you specifydurable=True, calling ‘atomic’ fails with an error, as it isn’t legal in this case.
In their own projects they sometimes wanted to have more explicit control. They
sometimes had a check like if assert not connection.in_atomic_block, but that didn’t
look nice. They split atomic() into separate transaction(), savepoint() and
durable() context managers. Nicely explicit.
The code is here: https://github.com/kraken-tech/django-subatomic
A big inspiration for them was a talk by Aymeric Augustin at the 2013 djangocon.eu. Here’s my summary of that talk.
Unrelated photo explanation: a cat I encountered in Athens on an evening stroll in the neighbourhood behind the hotel.
Djangocon EU: improving runserver with django-prodserver - Andrew Miller¶
2026-04-17
(One of my summaries of the 2026 Djangocon EU in Athens).
Original title: improving one of Django’s most used APIs — and it’s not the one you’re thinking of. (I’m using a more descriptive title for my blog entry.)
APIs are everywhere. Django rest framework, but also the models layer. And: manage.py
runserver, he considers that an API. Everybody runs it. So: can we improve it?
“Runserver” doesn’t sound the same as “devserver” or “rundevserver”. It doesn’t advertise that it is only intended for development. A name change could help there. But… Django really likes to be stable. It is probably too entrenched to change.
Production is missing a cohesive API. You normally run something like gunicorn
myapp.wsgi 0.0.0.0:8000 --workers 4…
He tried to get improvements in. Since 5.2, there’s a warning when you start
runserver: “it is only intended for development”. But you might miss it when
you get lots of log messages. Other people complained about the extra two lines.
He started django-prodserver.
pip install django-prodserver[gunicorn]. You can then run manage.py prodserver
(and manage.py devserver, he snuck that in).
You have to add a bit of configuration to your settings file:
PRODUCTION_PROCESSES = {
"web": {
"BACKEND": "django_prodserver.backends.gunicorn.GunicornServer",
"ARGS": {"bind": "0.0.0.0:8000",}, # add number of workers and so.
},
}
Run it with manage.py prodserver web. You can add more processes, for instance for
a background worker process.
It is the first version. He wants feedback, especially on the naming. manage.py
prodserver web or just manage.py web? And manage.py worker? manage.py serve
--prod?
Django isn’t just a framework, it is a set of APIs. We can prototype new APIs in packages.
Unrelated photo explanation: a cat I encountered in Athens on an evening stroll in the neighbourhood behind the hotel.
Djangocon EU: auto-prefetching with model field fetch modes in Django 6.1 - Jacob Walls¶
2026-04-17
(One of my summaries of the 2026 Djangocon EU in Athens).
There’s an example to experiment with here: https://dryorm.xterm.info/fetch-modes-simple
Timeline: it will be included in Django 6.1 in August.
The reason is the 1+n problem:
books = Book.objects.all()
for book in books:
print(book.author.name)
# This does a fresh query for author every time.
You can solve it with select_related(relation_names) or
prefetch_related(relation_names). The first does an inner join. The second does two
queries.
But: you might miss a relation. You might specify too many relations, getting data you don’t need. Or you might not know about the relation as the code is in a totally different part of the code.
Fetch mode is intended to solve it. You can append .fetch_mode(models.FETCH_xyz)
to your query:
models.FETCH_ONE: the current behaviour, which will be the default.models.FETCH_PEERS: Fetch a deferred field for all instances that came from the same queryset. More or lessprefetch_relatedin an automatic, lazy manner.models.FETCH_RAISE: useful for development, it will raiseFieldFetchBlocked. And it will thus tell you that you’ll have a performance problem and that you might need FETCH_PEERS
This is what happens:
books = Book.objects.all().fetch_mode(models.FETCH_PEERS)
for book in books:
# We're iterating over the query, so the query executes and grabs all books.
print(book.author.name)
# We accessed a relation, so at this point the prefetch_related-like
# mechanism ist fired off and all authors linked to by the books are
# grabbed in one single query.
You can write your own fetch modes, for instance if you only want a warning instead of raising an error.
Unrelated photo explanation: a cat I encountered in Athens on an evening stroll in the neighbourhood behind the hotel.
Djangocon EU: zero-migration encryption - Vjeran Grozdanic¶
2026-04-17
(One of my summaries of the 2026 Djangocon EU in Athens).
Full title: zero-migration encryption: building drop-in encrypted field in Django.
He works at Sentry. Huge site with a Django backend and thousands requests per second.
He had to add a new table to store 3rd party API credentials. Oh: should this be encrypted? Yes. But: each team has its own way to encrypt data. And there were at least 10 encryption keys here and there (as environment variables). And tens of places where encryption/decryption happens.
So: better to build a generic solution. Or use an existing generic solution. And yes,
there are multiple libraries. EncryptedCharField looked nice. But the problem was
all the existing data in the various places. Sentry is not a site that you can shut down
for a while, so you have to do it with zero downtime. This means you can never change an
existing column type.
A solution could be to add a new encrypted field next to the existing one. Then fill it and backfill it and make sure no new data is written to the old field and then you can remove the old field. But that’s quite a job with all the different locations that had to be changed.
A Field class in Django has get_prep_value() and from_db_value(). Those are
called before storing data in the database and after grabbing it from the database. You
could create a new CharField-like field and start to encrypt values in
get_prep_value and decrypt the other way.
You’d have to be able to recognise the old un-encrypted values. A solution: prefix
encrypted values with enc:. Also key rotation can be handled this way, by including
that in the prefix (enc:key2:).
But there’s also a bjson field. They solved that by encrypting the json and writing a json to the database with the encrypted json in a field and also the encryption key info.
The code is in the sentry repo .
Unrelated photo explanation: a cat I encountered in Athens on an evening stroll in the neighbourhood behind the hotel.
Djangocon EU: body of knowledge - Daniele Procida¶
2026-04-17
(One of my summaries of the 2026 Djangocon EU in Athens).
Athens! The thinking industry started here. Athens is often the origin if you follow ideas to the source.
Here also Socrates was found guilty (280-221) for “corrupting the youth” on trumped-up charges. Though… he made it is job to be a complete nuisance: exposing everyone’s hypocrisy and asking difficult questions. After the 280-221 vote he got to give a speech in reaction. After that, the vote on the actual punishment was 360-141 in favour of the death penalty. The speech must have been particularly irritating.
On to a different subject. He watched the recent launch of the NASA rocket that went to the moon. A marvel of technology. That was measured using body parts, being 322 feet tall. And the distance to the moon in miles. Why not the scientific meter and kilometer?
Plato already mentioned it. “Now take the acquisition of knowledge; is the body a hindrance or not, if one takes it into partnership to share an investigation”. And “when the soul tries to investigate anything with the help of the body, it is obviously led astray”.
0.098 km = 98 m = 98000 mm, a child can understand it. Pure rationality. But ask a Metric Martyr in the UK how many feet are in a mile and most of them won’t know.
The world seems to be divided in two camps:
Thinking, rationality, abstraction, unboundedness.
Bodies, materiality, tangibility, being rooted.
Wouldn’t Plato have loved a computer? Pure rationality, following its logic programming without fail?
What about those body-part-units? They’re not that weird actually. They’re rational Roman measurements:
A mile is 1000 Roman paces.
1 passus = 5 pedes (feet).
1/12 pes (feet) = 1 uncia (thus: inch).
(Note: according to the Greek, Romans are only good for stealing Greek ideas, building roads and killing people.)
On to another aspect. Why is Django’s documentation so good? Well, it has been prioritized from the start. It is complete, accurate, consistent, rational and well-structured: all Platonic values.
But Daniele also thinks the documentation is so good because it fits the human body.
The size has to be right. The limitations of our intelligence are the limits of our embodied intelligence. We can only grasp so much, mentally. A list can be too long. A page can be too long. If information is cut in too-small parts, you also can get into problems as you have to context-switch between pages too much. We tire mentally also because we tire physically.
The same applies to our body. Our hands and fingers can grasp objects. But it has to be of a certain size. Too big and we can’t grasp it. Too small and our fingers can’t pick it up.
We experience documentation in time and space. We move with it. How long have you been reading the Django documentation? “Where are you in the text?” We orient ourselves in text as if in a space or a building. We rely on the humanised rationality of structure. Sometimes you’re in a building and it is clear where you have to go and in other buildings you feel lost.
Django’s documentation is so good because of the quality of experience that it gives you. It is almost an embodied being that you can experience in space and time. Does it fit you? Do you notice it? The embodied nature of the work and intelligence that the Django community poured into the documentation?
Early Macintosh manuals had to explain new concepts and really tried to explain them in a human way. Scrolling being explained with help of an old book scroll, for instance. A floppy disk for storage as a floor plan of a building with a corridor and rooms.
Aldine Press (started 1494 in Venice) had a vision to print the old classics in a more accessible way. Books in the middle ages used to be big. And sometimes chained to the desk. Not really accessible. By printing them in smaller, lighter, more accessible formats, he wanted to make our “body of knowledge” more fitting to the human body.
You can see the bodily aspects of knowledge in our language:
Seizing/taking: grasp, comprehend, apprehend, perceive.
Measuring: ponder, weigh up, fathom.
Body movement: jumping to conclusions, intuitive leap, stumble/trip
Spatiality: understand, position
Mental space. When he asked a question of Russell Keith-Magee at a Django sprint, Russell would close his eyes and turn his eye inwards for a while. He would look at the Django codebase in his head and navigate it. Just like you yourself would navigate a city?
Being a programmer isn’t so different from being a human with a body in time and space. Look at questions you might have as a beginning programmer:
Which file or directory or window to be in.
Where to expect the output.
When to expect it.
Where to enter a command.
When to do something.
In what order to do things.
And then look at an experienced programmer. They seem to know where they are. They know their way around. They can move smoothly.
Closing comment: there are some uncanny features in software nowadays. As a human, we are used to having limits. But nowadays we have infinite scrolling, doomscrolling. And edgeless, endless, virtual cloud resources. And LLM indeterminism. Those are not inherintly bad, but it is different from what we’re used to. Is this still computing fit for the embodied mind?
Unrelated photo explanation: a cat I encountered in Athens on an evening stroll in the neighbourhood behind the hotel.
Djangocon EU: role-based access control in Django - how we forked Guardian - Gergő Simonyi¶
2026-04-16
(One of my summaries of the 2026 Djangocon EU in Athens).
He works for authentik (an “open-core, self-hosted identity provider”).
Note: in the talk he’ll mix “access control” and “authorization”.
In Django, every model gets some basic permissions for CRUD named after the app and the
model. You can ask a user object if it has a certain permission. That method, behind the
scenes, will ask all authentication backends with backend.has_perm(self, perm, obj).
The default will check whether the user has the permission or if the user belongs to a
group that has the permission. So that’s quite a query. The backends are queried in
turn. If a backend doesn’t know if a user has a permission, it can just return None,
the next backend will then be checked. If the backend knows the user has no access,
it raises PermissionDenied.
The backends have methods like .has_perm(self, perm, obj), but “obj” isn’t normally
called, it is None by default. But you can implement it if you want object permissions.
Django-guardian is an implementation of object permissions for Django by providing an
extra authentication backend: ObjectPermissionBackend.
user.has_perm("change_book") asks if the user has the permission to change all
books. user.has_perm("change_book", obj=my_book) asks for a specific book.
Generic permission mechanisms deal with user, group, permission. Object permissions add userobjectpermission and groupobjectpermission. Well, that’s still reasonably OK.
But then Enterprise comes along with even more wishes:
Just-in-time privileged access.
Delegating permissions. Someone has a permission through my permissions. If I lose them, they lose them.
Custom permissions.
Group hierarchy.
Permission inheritance through group hierarchy.
Especially the group hierarchy was a problem. One time, an enterprise they worked with had a group that, after a couple of more groups, was a member of itself… And: django-guardian used Django’s group concept, which they couldn’t adjust.
So they started modifying django-guardian to use a custom Group model. They also made some other changes by emphasising a new Role concept and using Role to tie users/groups to permissions:
User can be in a group.
Groups can be nested.
A user (directly) or group can have a role.
A RoleObjectPermission references a role and a permission.
Lastly they added RoleModelPermission, replacing django’s model permission mechanism.
The core SQL query is only 52 lines, so that’s not bad. Even with 10k roles and 10k users, the query was below 1ms.
The fork is here: https://github.com/goauthentik/authentik/tree/main/packages/ak-guardian
His slides: https://github.com/gergosimonyi/djangocon-eu-2026/
Unrelated photo explanation: a trip in November to the Mosel+Eifel region in Germany. The “Oberburg” castle ruin in Manderscheid.
Overview by year¶
Statistics: charts of posts per year and per month.
- Weblog entries for 2003
- Weblog entries for 2004
- Weblog entries for 2005
- Weblog entries for 2006
- Weblog entries for 2007
- Weblog entries for 2008
- Weblog entries for 2009
- Weblog entries for 2010
- Weblog entries for 2011
- Weblog entries for 2012
- Weblog entries for 2013
- Weblog entries for 2014
- Weblog entries for 2015
- Weblog entries for 2016
- Weblog entries for 2017
- Weblog entries for 2018
- Weblog entries for 2019
- Weblog entries for 2020
- Weblog entries for 2021
- Weblog entries for 2022
- Weblog entries for 2023
- Weblog entries for 2024
- Weblog entries for 2025
- Weblog entries for 2026
- Tag overview