21 Jun 2026
Planet Python
The Python Coding Stack: 2. Anatomy of an Agent
Read Stephen's Preface to Agents Unpacked if you're new here.
You have used a large language model. You know the deal: a careful prompt gets a careful answer. A vague prompt gets a vague one. And the model itself does not keep anything from one conversation to the next, unless something external is holding that context for it.
Agents work differently. They have parts that do things a plain LLM does not. These parts are what make an agent an agent. It is not just the model underneath. It is the structure built around it that gives the system its abilities to persist, act, and keep going.
Understanding this structure is the second major shift in this series. The first shift is seeing that a chatbot can give you a good answer without finishing the job, because it stops after responding. The second shift is seeing that an agent is not a smarter model. It is a model placed inside a structure that gives it something to act with and somewhere to keep what it has done.
The Agent Formula
Most agents share the same basic parts:
-
A model (the LLM): the reasoning engine that understands language and decides what to do
-
Instructions: what tells the agent who it is, what it is for, and what 'good' looks like
-
Memory: a workspace or store that holds what has happened so far
-
Tools: capabilities the agent can call on to do things beyond generating text
-
An execution loop: the cycle of observing, deciding, acting, and checking
Different platforms package these differently. Some call memory "context," some call tools "plugins" or "capabilities," and some merge instructions and tools into a single configuration layer. But the parts are the same. An agent is not a single thing. It is a system, and each part matters.
Stephen: Don't LLMs also have memory since they remember what happened earlier in the conversation? How's this different?
Here is one distinction worth getting clear early: the context window and memory are not the same thing. The context window is the working space an LLM uses during a single session. It holds the conversation so far and gets loaded fresh every time the model gets a chance to speak. Memory, by contrast, is information stored outside the model, maintained by the system, and available across sessions and steps. We will come back to this.
An agent needs all its components:
Agent = Model + Instructions + Memory + Tools + Execution Loop
Leave any one of these out and the system changes behaviour in ways that matter. We will look at each piece in turn.
What the Model Does and What It Doesn't Do
The model is the reasoning core. It reads your request, figures out what to do, and decides what to say back. It gets the most attention because it is the part that generates language.
But a model on its own is like a brilliant mind with no hands and no memory of its own. It can think. It cannot act. It cannot remember what happened five minutes ago unless something explicit carries that information forward.
Stephen: Wait a second. You say the model doesn't remember what happened five minutes earlier. But when I use an LLM, it does seem to remember what happened earlier in the conversation.
Here is what is actually happening. When an LLM appears to remember earlier in a conversation, it is not the model itself that is remembering. The context window is carrying all the earlier messages along with your new message, every time you send something. The model sees the full conversation again and generates a response that fits what came before. That is not memory in the model. That is the system feeding the model a transcript.
This trips up almost everyone when they start using agents. The model generates text. The rest of the system decides what to do with that text and whether to act on it.
A better model helps. It reasons more clearly, follows instructions more faithfully, and handles edge cases better. But dropping a smarter model into an agent that is missing a working execution loop will not make it an agent. You need the other parts too.
Instructions: The Agent's Direction
Instructions tell the agent what it is supposed to do and how to behave. Some systems call these system prompts. Others call them agent definitions or behavioural instructions. The name does not matter. What matters is that they are the layer that tells the model why it exists, who it is helping, and what 'good' looks like for the task at hand.
Good instructions do not make an agent smarter. They make it more focused. They give it a frame for every decision: what to prioritise, what to avoid, when to ask for help, how to present its output.
Stephen: Are these what are often called 'skills', or are skills something else altogether?
Skills and instructions are related, but they are not the same thing. Instructions are the core behavioural direction: who the agent is, what it is for, how it should approach its work. A skill, in platforms like OpenClaw and Hermes, is a specific file that tells the agent how to carry out a particular task, often by combining one or more tools. So instructions tell the agent how to behave generally. A skill tells it how to do something specific. We will see this distinction more clearly when we look at how different platforms implement these parts.
The instructions shape what the agent notices, what it proposes, what it tries, and what it says no to. Two agents built on the same model with different instructions will behave differently in the same situation. They will notice different things, prioritise differently, and produce different outcomes.
Poorly written instructions can quietly break an agent. If the instructions are vague, the agent has to improvise every step. If they contradict each other, the agent has to choose, and it might not choose the way you intended.
Stephen: Can you provide a few examples of what these instructions may look like in different scenarios?
Here is what instructions might look like in practice. A poorly-written instruction can quietly break an agent. Consider an instruction that says "be helpful and concise" without defining either term. When a user asks for a full technical breakdown, the agent has to arbitrate between two vague goals. It might give a two-sentence answer that technically satisfies "concise" but ignores "helpful," or it might give an exhaustive response that satisfies "helpful" but ignores "concise." Either way, the agent is improvising because the instructions gave it no real frame for the conflict.
A research assistant agent might have instructions that say something like: "You are a research associate working for [user name]. Your role is to find, summarise, and organise information on topics the user assigns. Always cite your sources. Flag uncertainty rather than guessing. Present findings in a clear brief, not a wall of text."
A code review agent might have very different instructions: "You are a principled code reviewer. Focus on correctness, clarity, and performance. Do not praise code unnecessarily. When you find an issue, explain why it matters and suggest a concrete fix. Keep responses short."
The difference between those two sets explains a lot about why two agents can feel like entirely different systems, even if they use the same model underneath.
Memory: The Workspace and Context
Memory in an agent is not like human memory. It is a structured store of information kept and updated as the agent works. It is what lets the agent hold a thread across multiple steps without starting from scratch each time.
Most agents use some combination of three types:
-
Working context - what is active right now: the current goal, what has been tried so far, what the user last said
-
Stored information - what the agent has been told about the user, their preferences, their past requests
-
Files and state - what exists in the workspace right now, what has been written or read recently
This is not a personality feature. It is not the agent "remembering" in the way a person remembers their childhood. It is operational continuity. The system maintaining a thread of relevant information across time and steps.
Different platforms handle these differently. LangChain agents build up a rolling context window: the current request gets appended to everything that happened before, and the whole thing is passed to the model. If the conversation gets long, older turns get dropped or summarised to make room. AutoGen agents can maintain shared memory across a team, so that when one agent finishes a task, what it learned is available to the next agent that picks up the thread.
OpenClaw takes yet another approach. Its memory layer is a structured store that agents write to and read from across sessions. When an agent starts a new session, it can query that memory store for relevant context rather than relying solely on what was in the most recent conversation. An agent can know that the user prefers short emails, even if that was established three weeks ago.
Stephen: If memory can be stored in files, does it mean that agents can have nearly unlimited memory (within the limits of the computer or server's overall memory capacity)?
There are practical limits even when storage is effectively unbounded. The more relevant limit is not how much the agent can store, but how well it can find and use what it has stored. A full inbox is not the same as a well-organised one. Retrieval becomes harder as memory grows, and irrelevant information can dilute the signal if the system does not manage it carefully.
Think of it this way. A context window that holds 128,000 tokens can technically hold a lot of information. But it can only hold what was placed there. An agent with a large memory store full of useful context still needs a way to surface the right information at the right time. If it cannot find what it needs, or if what it finds is buried under noise, the effective memory is constrained.
The quality of retrieval matters as much as the quality of storage. An agent that retrieves relevant context poorly is effectively working with a much smaller memory than one that retrieves well, even if both store the same amount.
Stephen: So, tell me if I understood this. The agent has an index telling it where to find information specific to certain topics or tasks. When the LLM part of the agent decides it needs to deal with a certain topic, it uses the index to read and load the information from the memory file into its context. Is that right?
That is broadly right. The memory store, the index, and the retrieval into context are the key parts. One small correction worth noting: the decision to retrieve from memory is typically made by the agent or coordinator layer, not by the LLM directly. The LLM receives the retrieved content as part of its context, but it is the agent system that decides what to look up and when. This distinction matters because it is the agent layer, not the model, that is doing the memory management.
Stephen: But isn't the agent's brain the LLM? Clarify the distinction in your answer above. Which part of the agent's infrastructure deals with this?
It is a fair challenge. The LLM is genuinely where the reasoning happens. It reads context, generates text, and makes decisions about what to say or do next. But it is also just a text processor. It receives input, produces output, and has no awareness of anything beyond the tokens it has been given.
The coordinator layer is the infrastructure that sits around the LLM and manages the process. It reads the LLM's output, decides whether to act on it, calls tools, retrieves memory, and feeds results back into the next LLM call. It is the difference between the LLM thinking and the agent doing. A bare LLM generates text. The coordinator turns that text into action.
To use a rough analogy: the LLM is like a pilot who can read instruments and make decisions. The coordinator is like air traffic control - it decides which runway to use, when to land, and when to divert. The pilot's brain does the reasoning. But without the infrastructure around it, the pilot just sits in the cockpit thinking.
So when we say the agent retrieves memory, we mean the coordinator retrieves it and places it where the LLM can see it. The LLM does not reach into a file and pull something out. The coordinator does that work and presents the result to the LLM as part of the next context.
Stephen: And are the bits of these files then loaded into the LLM's context? Therefore, the more stuff is loaded from the memory files, the more the context fills up, affecting the rest of the conversation and cost, right?
Yes, exactly right. Memory retrieval feeds into the context window, which is the LLM's working space for the current session. Every token that goes into the context window is a token the LLM processes and a token that costs something. Loading a lot of context from memory means less room for the conversation itself, and it means higher token usage on every call.
This is one of the practical engineering tensions in agent design. Loading more memory gives the agent more to work with, but it also makes each LLM call more expensive and slower. A well-designed agent retrieves only what is relevant to the current task, not everything it knows.
Tools: What the Agent Can Actually Do
Tools are the capabilities that let an agent act beyond generating text. The model decides to use a tool. The tool performs an action and returns the result to the model.
This was covered in Chapter 1 under "Tools Are the Hands." Here it is worth noting that tools are also where agents differ most between platforms. Some agents come with a large built-in toolkit. Others can call external tools through open protocols. Some let you build custom tools. Others are more locked down.
What tools might an agent actually have? A research agent might be able to search the web and read files on your machine. A coding agent might run shell commands and read or write files. A calendar agent might check your schedule and send messages. The tool is the bridge between the model's decisions and the world the agent is working in.
What matters is not how many tools an agent has, but whether the tools it has are the right ones for the tasks you want it to perform.
Different platforms implement tools differently. LangChain provides a standardised tool interface that lets you connect to search APIs, databases, file systems, and custom functions. OpenCode agents run inside a development environment, where the tools available are the commands and interfaces of that environment. OpenClaw uses an open tool protocol that lets agents call external capabilities regardless of who built them. Hermes takes a more composed approach: a skill file specifies not just what the agent should do, but which tools to use and in what combination to carry out a specific task.
Here is the thing worth unpacking. A tool on its own is just a capability. What makes it useful is the bridge between what the agent is trying to accomplish and the tool that can help. A calendar tool is useless if the agent does not know it should check the schedule. An agent running a meeting-preparation skill that says "check availability, send invites, prepare a briefing document" has that bridge built in.
The Execution Loop: The Part That Makes It an Agent
The execution loop is the cycle that takes an agent from a single-shot response to a sustained process. Observe, think, act, check, repeat.
This was the core of Chapter 1. But it is worth restating here, in the context of anatomy, because the loop is what ties all the other parts together. Without it, you have a model that receives instructions and context and produces text. With it, you have a system that can pursue a goal across time, recover from partial failures, and stop when the work is genuinely done.
The loop is the difference between an agent and a very well-instructed chatbot.
Here is why the repeat step matters so much. A model has no native sense of when it is done. When you call a function in code, the function returns and you are finished. When a model generates text, it produces tokens until it hits a stop condition built into the model itself, most commonly a token limit or a designated stop sequence. These conditions tell the model when to stop generating, but they do not tell the agent whether the result is actually what the user wanted. There is no built-in check that says "is this the right answer?"
The execution loop provides that check. The check phase asks: is the result good? Does it meet the original goal? If not, the loop continues. Sometimes that means a dozen or more cycles before a task is genuinely complete.
The loop also determines how goals decompose. In LangChain's ReAct-style agents, the loop runs inside a single agent: observe, decide on the next action, execute it, check the result, repeat. In AutoGen, the loop is distributed across multiple agents that hand off to each other. A planner agent might coordinate specialist agents, each running their own loop on their own piece of the problem. OpenClaw uses a coordinator agent to manage the loop, assigning work to sub-agents and handling the check phase across the full task rather than within a single agent cycle.
The architecture of the loop is one of the most significant differences between agent platforms. But the function is the same everywhere: turning a sequence of isolated model calls into a coherent, goal-directed process.
Multiple Platforms: Comparing the Formula in Practice
It helps to see the same five-part formula playing out in different platforms. Here is how a few of them map onto it.
LangChain is one of the most widely-used agent frameworks. A LangChain agent has an LLM at its core, a set of tools, a prompt defining the agent's role, memory that accumulates conversation history, and an agent executor that runs the loop. The loop in LangChain is explicit: the agent executor repeatedly calls the model, parses the model's tool-call output, runs the tool, and feeds the result back until the model says it is done.
AutoGen takes a different approach. Rather than a single agent, AutoGen sets up a team of agents that communicate with each other. Each agent has a model, instructions defining its role, and its own set of tools. The loop is distributed: there is no single execution cycle. Agents exchange messages, delegate tasks to each other, and the overall process continues until the team has finished the assigned goal. Memory in AutoGen can be shared across agents so that one agent's work is available to the next.
OpenClaw uses a coordinator agent that manages the overall execution loop. Sub-agents each have their own identity, tools, and memory. The coordinator decides which sub-agent handles which part of a task, passes context between them, and handles the check phase across the full goal. Skills in OpenClaw are files that tell a specific agent how to carry out a particular task, combining instructions about what to do with definitions of which tools to use.
Hermes also uses a skill-based architecture where skill files define both the instructions and the tool configuration for specific tasks. Rather than a single general-purpose agent, Hermes composes agents from skills that know how to use particular tools in particular contexts.
OpenCode works differently again. It runs agents inside a development environment, typically a cloud workspace. The tools available to the agent are the commands and interfaces of that environment. The loop is typically managed at the task level: the agent receives a task, works through it using the tools at its disposal, and reports back. There is less of a formalised multi-step loop and more of a task-completion focus.
None of these platforms invents new parts of the agent formula. They all use a model, instructions, memory, tools, and an execution loop. What differs is how those parts are implemented, how they are divided up, and how they communicate. Understanding the formula means you can look at any of these platforms and see what you are actually looking at.
What This Chapter Covered
This chapter pulled apart the five components of the agent formula.
We saw how the model is the reasoning core but cannot act or remember on its own. How instructions shape the agent's focus and behaviour, and why the same model with different instructions can feel like a different system entirely. How memory provides operational continuity across steps and sessions, and why retrieval quality matters more than storage capacity. How tools extend what the agent can do beyond generating text, and why a tool is only as useful as the bridge between the model's decisions and the action the tool can take. And how the execution loop is the architecture that turns isolated model calls into a coherent, goal-directed process.
We also saw how different platforms implement the same five components differently: LangChain's explicit agent executor, AutoGen's team-based coordination, OpenClaw's coordinator and skill-based sub-agents, Hermes's composed skill architecture, and OpenCode's environment-integrated approach.
The goal was not to become an expert on any one platform. It was to show that agents are not mysterious black boxes. They are systems built from a small number of recognisable parts, and once you know what to look for, you can see the anatomy underneath any agent platform you encounter.
Next up in Agents Unpacked: we dig into tools and skills: what it actually means for an agent to do something rather than just say it, and why a well-tooled agent operating autonomously in a loop is a fundamentally different thing from a model answering questions.
<< Previous Post: From Answer to Outcome
>> Next Post: Coming Soon
21 Jun 2026 9:48pm GMT
Christian Ledermann: Stop Copying Your .pre-commit-config.yaml
Stop Copy-Pasting: Introducing pc-init
We've all been there: you're starting a new project, you've got your repo initialized, and now comes the tedious part-setting up the quality gates.
You know you need pre-commit (or the newer prek) to keep your code clean, but you end up hunting through your older repositories to find the "best" .pre-commit-config.yaml to copy and paste. Then, you spend ten minutes editing paths, versions, and configurations to match the specific needs of your new stack.
It's a chore that breaks your flow before you've even written a line of code. That is exactly why I built pc-init.
The Problem: The "Config Archeology" Workflow
Most languages and frameworks have a gold standard for quality tools. If you're working in Python, you want ty and ruff; in React, you want eslint and prettier.
Manually setting these up requires:
- Identifying the recommended tools for your specific stack.
- Looking up the correct hook repository URLs, revisions, and arguments.
- Writing the YAML by hand (and hoping you didn't miss a syntax error).
Doing this every other month is just frequent enough to be a persistent pain point, but not frequent enough to have the workflow committed to muscle memory.
The Solution: pc-init
pc-init is a CLI tool designed to replace "config archeology" with a single, declarative command. It scaffolds a production-ready .pre-commit-config.yaml based on your project's technology stack.
Instead of copying old files, you simply tell pc-init what you're building, and it builds the configuration for you.
How it works
pc-init uses a system of language and framework presets. You provide the parameters, and it handles the rest:
# For a standard Python project
pc-init --lang py
# For a JavaScript project using React
pc-init --lang js --framework react
# For a Python project using Django
pc-init --lang py --framework django --force
Why use pc-init?
- Zero Boilerplate: No more hunting for URLs or managing complex YAML structures.
- Standardization: It ensures consistency across all your projects by enforcing the same quality standards.
- Extensible: Not happy with the default presets? You can point
--presetsto your own local directory or a private git repository to share your team's custom standards across your entire organization. - Flexible: It works seamlessly with both pre-commit and prek.
Get Started
If you're ready to speed up your setup process, you can install pc-init via uv:
uv tool install pc-init
After generating your config, don't forget to run pre-commit autoupdate or prek autoupdate to ensure you are pulling the very latest versions of your selected tools.
If you have suggestions for new presets or run into issues, please head over to the GitHub repository and open an issue. Let's make boilerplate setup a thing of the past.
21 Jun 2026 10:53am GMT
Bob Belderbos: From Python to Rust: Master Iterators by Rebuilding 10 Unix Tools
The fastest way I know to learn a language is to rebuild something you already understand. You stop fighting the problem and spend your attention on the syntax and the idioms. That is the whole idea behind the new Unix tools track I just released on the Rust platform: ten small command-line classics, each one a pure function you implement and cargo test to validate you got it right.
Why Unix tools, and why from Python
Every exercise opens with the Python you would write, then teaches the Rust idiom that replaces it. People who love the platform keep pointing at the same thing: the Python-to-Rust bridge is what makes the concepts stick.
You are not memorizing Iterator methods in the abstract, you are watching len(text.split()) turn into .split_whitespace().count(), and reaching for Option and Result instead of Python's exceptions.
Take cut. In Python an invalid field raises an exception at runtime:
def cut(text, delim, field): # field is 1-based
if field == 0:
raise ValueError("field values may not include zero")
...
In Rust both outcomes live in the return type:
#[derive(Debug, PartialEq)]
enum CutError {
ZeroField,
}
fn cut(text: &str, delim: char, field: usize) -> Result<Vec<&str>, CutError> {
// ...
}
#[test]
fn field_zero_is_an_error() {
assert_eq!(cut("a:b", ':', 0), Err(CutError::ZeroField));
}
Encoding the failure in the type, not just the docs, is a core Rust idea: the compiler makes every caller account for it, and that is a big part of what makes Rust code safer. The failing case becomes a test you code towards.
Iterators are the spine of the track, seven of the ten exercises turn for loops and comprehensions into iterator chains. Every exercise relates back to one or more Python idioms you already know.
For me Rust is hard but thanks to comparison with Python I feel that I understand more. - Piotr R
Having a direct comparison with Python snippets keeps me more in the context of what's going on. - Michal S
The track also follows one rule that matters for real CLIs: the logic lives in a pure, testable function, while the I/O (reading a file or stdin) stays in a thin wrapper around it.
That split is exactly how you would structure a professional Rust tool, and it is why every exercise can be validated by a test instead of by running a binary. I wrote more about how this rewiring changes your Python instincts in Rust made me a better Python developer.
The 10 exercises and what each one teaches
-
wc: count lines, words, characters. Iterators plus.count()replacelen(...), and you return a(usize, usize, usize)tuple. The character count is also a sneak intro to whychars().count()is notlen()in a Unicode world. -
head&tail: first and last N lines.headuses lazy.take(n)and stops early;tailforces you to collect first and slice from the end, because you cannot run an iterator backwards. -
cat -n: number the lines. Rust's.enumerate()starts at0, not1(there is nostart=argument), and you rebuild the numbered text from there. -
tr: translate and delete characters. The exercise that makes thecharvs&strdistinction click:'l'is achar,"l"is a&str, and you.map()over.chars()to rewrite each one. -
grep: filter matching lines, with-iand-v. Substring tests with.contains(), case folding, and a single boolean condition that handles both-iand-vwithout branching. First taste of borrowing and lifetimes in the signature.

-
cut: extract a field. Two outcomes that pull in different directions: a missing field is normal (skip the line), butfield == 0is a bad request. You model that withOptionandResultinstead of Python's exceptions. -
uniq -c: count adjacent duplicates. Rust has noitertools.groupby, so you walk runs by hand with pattern matching and aVec, keeping the consecutive-only behavior honest. -
sort: sort lines, with-n. Iterators do not sort, so youcollectinto amut Vecand call.sort(); numeric order meanssort_by_keywith a closure, Rust's answer to Python'skey=. -
sed s///: find and replace per line.str::replaceswaps every match,replacenstops after a count, so thegflag is just a choice between two methods, no manual counting. -
top_words: the capstone. Compose everything: count word frequencies with theHashMapentry API (entry().or_insert(), since there is noCounter), then sort and take the topn. This is thetr | sort | uniq -c | sort -rn | headpipeline rebuilt as one Rust function.
If you have been meaning to get past reading about Rust to actually writing it and making the concepts stick, begin with the free wc and head / tail exercises. Each one is short, has tests to code towards, and there is no AI on the platform; you have to do the work. I hope you learn a lot: start the Unix tools track.
Next up on the platform: a track on Rust lifetimes.
21 Jun 2026 12:00am GMT
19 Jun 2026
Django community aggregator: Community blog posts
Issue 342: DSF Executive Director Search
## News
Announcing the Search for a DSF Executive Director
The Django Software Foundation is hiring its first Executive Director, and we have the Django community to thank for making it possible.
Six Django web development agencies have jointly pledged $47,500 to help fund the Executive Director's first year: Caktus Group, Lincoln Loop, Six Feet Up, Cuttlesoft, OddBird, and Two Rock. This is the financial foundation we needed to move from "we should hire an ED someday" to "we are hiring an ED now."
I'm delighted to rejoin the Sovereign Tech Fellowship
Hugo van Kemenade returns to the Sovereign Tech Fellowship after being one of six participants in the 2025 pilot, calling out how dedicated time helped ship Python 3.14 and 3.15 releases, mentor triagers, and improve release automation and accessibility. The post also tracks a wide set of community and governance work, and looks ahead to a larger 2026 cohort spanning maintainers, community managers, and technical writers.
Python Software Foundation
Python Software Foundation News: PSF Board Election Dates for 2026
PSF Board elections for 2026 open for nominations on July 28 (2:00 pm UTC) and voting runs September 1 to September 15, with voter affirmation due August 25. The Packaging Council election will run in parallel under PEP 772, and PSF member voting eligibility is handled via psfmember.org.
Updates to Django
Last week we had 24! pull requests merged into Django by 11 different contributors.
This week's Django highlights ๐ฆ:
-
Added --using option to sendtestemail management command. (#37141)
-
As a performance optimization, add an option to cull the DBCache only on every n queries. (#32785)
-
Reduced false positives in strings during collectstatic. (#36969, #35371)
Sponsored Link
Reach 4,300+ Engaged Django Developers
Sponsor this newsletter to reach an active community of Python and Django developers.
Articles
In search of a new contribution model
Carlton Gibson on why open source's contribution model is broken--burnout, extractive contributions, harassment, and now AI--and his plans to experiment with something less open-by-default on newer projects.
The University In The AI Era
From Carson Gross, creator of HTMX and full-time college professor, a detailed and practical look at what AI means for universities in general and computer science programs in particular.
How I Work From Anywhere Without Losing My Place
Jeff has been running a new remote dev setup that allows for seamless switching between home office, an iPad, or even a phone when out on the go.
LLM-Inspired Development
How a bad idea from an LLM led to a good idea on a website.
Tech doesn't matter? Why to use Django for agentic coding
Ronny Vedrilla argues that in the age of agentic coding, Django's opinionated structure, secure-by-default posture, and heavy representation in training data make it an ideal "harness" that keeps AI agents on the rails-not a competitive edge, but a hedge against shipping a quiet disaster.
Videos
The Modern Python Web Stack: Django, FastAPI, uv, Pydantic, and AI
A 5-minute conversation from PyCon US with Jeff Triplett on how Python web development is changing fast. (Yes, this video features Jeff and Will, the two authors of this newsletter, but we still think it warrants mention! ๐ค)
Podcasts
Teaching Python #158: Will Vincent on Django, AI Coding, and Why Fundamentals Still Matter
A chat on why Django continues to matter, the reality behind vibe coding, local AI models, and more.
Django Forum
Call for mentors - GSoC 2026 with Django!
Google Summer of Code is around the corner and there is still a need for mentors on some projects.
Ticket 34753, Document how to properly escape to in email messages
An active discussion around this particular issue. Checking the forum is a great way to get a pulse on what's happening with core Django development.
Django Fellow Reports
Jacob Walls
In this four-day week (I headed out Friday for a college reunion), everything got a little bit better. First, check out @blighj's estimate showing that collectstatic's import statement detection reliability (needed to rewrite URLs) improves in Django 6.1 from 88% to 99%. Meanwhile @felixxm is stress-testing database defaults and landing fixes needed for using Django 6.1's UUID4()/UUID7() functions. Finally, we made the test client more friendly for third-party permission packages like django-guardian and django-rules. @sage also spotted a breakage in DRF in the upcoming Django 6.1 beta, since Wagtail tests against Django's main branch. I expect the fix to land before the beta is even out. Be like wagtail and test main!
Natalia Bidart
This week had a bit of a reset feel to it ๐งน. After the previous stretch of PyCon US, security prep, and the security release itself ๐, I spent time going through pending and snoozed items โฐ, trying to close loops and get things back to a more manageable state.
We also reviewed and triaged a batch of security reports ๐ that were shared by a major AI company, following conversations I had at PyCon US ๐ ๐๏ธ about the growing volume of LLM-generated security submissions and the challenges they create for OSS projects (Django in particular). The reports were generated using an advanced security-focused model ๐ค against the Django codebase. We evaluated each finding, confirming and addressing valid issues where appropriate and mapping others to existing tickets and prior reports. Overall, Django is in good shape ๐ช, as the results largely overlapped with known reports, validated our current triage approach, and reinforced confidence in our security stance ๐.
Events
Django Girls Krakow on 18th July 2026
This event is taking place during EuroPython at the sprints venue.
Django Day Copenhagen 2026
Djangonauts from in and around Denmark are meeting up for the second edition of Django Day Copenhagen 2026, October 2.
International Travel to DjangoCon US 2026
Are you attending DjangoCon US 2026 in Chicago, Illinois, but you are not from US and need some travel information? Here are some things to consider when planning your trip.
Join DEFNA! There's a seat on the DEFNA board open
Django Events Foundation North America (DEFNA) is looking for another board member. We have eight board members currently and are looking for another person passionate about growing the DjangoCon US community to join.
Django Job Board
Senior Python/Django Developer at Gryps ๐
Founding ML/Data Scientist (Remote, UK) at MyDataValue
Projects
ranahaani/GNews
A Happy and lightweight Python Package that Provides an API to search for articles on Google News and returns a JSON response.
jazzband/django-newsletter
An email newsletter application for the Django web application framework, including an extended admin interface, web (un)subscription, dynamic e-mail templates, an archive and HTML email support.
19 Jun 2026 3:00pm GMT
17 Jun 2026
Django community aggregator: Community blog posts
The 2026 way of using importmaps in Django
The 2026 way of using importmaps in Django
I last wrote about Django, JavaScript modules and importmaps in May 2025, slightly over a year ago.
The main topic of this post is the django-js-asset 4.0 release. The library is used in many places, some of the more well-known packages using it are django-mptt and django-ckeditor. I have since done a lot of work evolving the ways of integrating importmaps but the efforts to standardize upon an approach have stalled a bit. The main reason for this, apart from time and energy, was that I wasn't really all that happy with the global importmap. When I had only a few modules using the importmap facility, I didn't care all that much. Now that the recently released django-content-editor 9.0 also uses importmaps for shipping a refactored, much more modular JavaScript implementation while still keeping all the benefits of cache busting using ManifestStaticFilesStorage1, having a global importmap got annoying. The content editor JavaScript is only used within the Django administration interface, but when using a single global importmap object, the importmap entries were always there on each page that used an importmap at all.
A better solution was needed. I'm a big fan of using forms.Media for collecting CSS and JavaScript from widgets, forms and utilities. It helps me avoid inline JavaScript since at least 2017. I'm not using it for site-wide CSS and JavaScript, I'm still transpiling, PostCSS-ing and bundling the assets using rspack as for example written about here and here.
Why importmaps?
A quick refresher on why this matters at all. Django's ManifestStaticFilesStorage hashes the contents of each file into its name for cache busting, but out of the box it doesn't rewrite the import statements inside JavaScript modules. Importmaps bridge the gap: your code imports a stable name:
import { initializeEditors } from "django-prose-editor/editor"
and the importmap tells the browser where that name actually lives:
<script type="importmap">
{"imports": {
"django-prose-editor/editor": "/static/django_prose_editor/editor.6e8dd4c12e2e.js"
}}
</script>
So the import stays clean and constant while the file behind it can get a new hash on every deploy.
django-js-asset 4.0
The updated django-js-asset 4.0 doesn't ship the old, global importmap at all. This means the upgrade might require some work. Instead of one importmap shared across the whole site, you now get a specific importmap assembled for the context at hand - either by Django itself when it collects the media of your forms, widgets and the admin, or explicitly by you in a view or context processor. The building block in both cases is the ImportMap object; when it travels through js_asset.Media (a subclass of django.forms.Media) the maps are automatically merged into a single <script type="importmap">, by customizing and extending what Django does already when merging media instances.
The release notes go into more detail.
In practice
If you're using a package such as django-prose-editor in the Django admin you don't have to do anything, things should just work.
If you're using such a package outside the admin, you have to remove "js_asset.context_processors.importmap" from your list of context processors. On one particular website the prose editor is the only package with importmap entries outside the admin, so I have to add the importmap to the template context myself:
from django_prose_editor.widgets import importmap
def view(request, ...):
return render(request, "template.html", {
# ...
"importmap": importmap,
})
The template then just renders it in the <head>:
... {{ importmap }}</head>
On a different site, I have a slightly more involved scenario where I previously used importmap.update(...) to add my own entries to the importmap. There, I'm using a custom context processor to always add these entries to the importmap too:
from django_prose_editor.widgets import importmap as dpe_importmap
from js_asset import ImportMap, static_lazy
_site_importmap = ImportMap({
"imports": {
"my-module": static_lazy("my-module.js"),
}
})
_importmap = dpe_importmap | _site_importmap
def importmap(request):
return {"importmap": _importmap}
This importmap is merged once at server startup and then served repeatedly to the client. Because we use the lazy version of the static function we can do this during startup and not worry about files not yet collected by collectstatic - we'll get the correct paths later.
On the same site as the previous example, I also have an admin inline which requires some JavaScript and also an importmap:
from django.contrib import admin
from django.forms import Script
from js_asset import Media, ImportMap
# Initializing this once. Not necessary but I like it better that way.
_importmap = ImportMap({
"imports": {
# ...
}
})
class ModelInline(admin.StackedInline):
@property
def media(self):
return Media(
js=[
_importmap,
Script("module.js", type="module"),
]
)
As of 4.0, JS and CSS produce Django's own Script and Stylesheet objects, so you can import and use Script directly from django.forms as shown above (on Django 4.2-5.1, import it from js_asset instead, which backports it). The familiar JS("module.js", {"type": "module"}) wrapper still works unchanged if you prefer it - it just takes a positional dict instead of keyword arguments.
Here, it's really important to use the js_asset.Media and not django.forms.Media. js_asset.Media knows how to handle importmaps - all importmaps are collected from all media lists, merged and added to the output before all other CSS and especially JavaScript. The reason for that is that browsers only honour a single importmap per page, and it really has to appear before all JavaScript modules referencing any entries in the importmap.
The nice thing about js_asset.Media is that it doesn't have to appear first in the list of media classes which are merged - it can also appear in the middle or last, and still can do its magic after all Media objects have been merged into a single one.
The rest is handled by Django itself, since it already supports collecting media assets. The missing piece was just the importmap object and the js_asset.Media class which knows how to special case them, and which - through the power of overriding __add__ and __radd__ takes over all the other media instances.
What's next
I haven't yet used CSP nonces using {% csp_nonce_attr media %} in production myself, but it should just work, even with importmaps and everything else. Given that I have a passing test suite I have no reason to believe it doesn't already work, but I'd like to have a confirmation.
I'm hoping to standardize some more. If we could get something like this in Django core that would be really nice. Maybe I'll be able to work on that at Django on the Med ๐๏ธ. Since no browser supports multiple importmaps as of today having multiple implementations of importmaps in the Django ecosystem will lead to trouble down the road. I think there is a clear case to be made for importmap support in Django and I would obviously love it if the approach implemented today in django-js-asset would be the basis for the official solution.
-
Without having to do any overrides to enable ESM support. โฉ
17 Jun 2026 5:00pm GMT
16 Jun 2026
Django community aggregator: Community blog posts
Cheating as a programming discipline
Great programmers cheat. A hard problem gets quietly swapped for an easier one; a transaction-grade database is replaced by a flat file nobody misses; machinery everyone else considers mandatory simply never gets built. They know a lot - and that's exactly why they get away with it.

16 Jun 2026 11:00am GMT
09 Jun 2026
Planet Twisted
Hynek Schlawack: How to Ditch Codecov for Python Projects
Codecov's unreliability breaking CI on my open source projects has been a constant source of frustration for me for years. I have found a way to enforce coverage over a whole GitHub Actions build matrix that doesn't rely on third-party services.
09 Jun 2026 12:00am GMT
22 May 2026
Planet Twisted
Glyph Lefkowitz: Opaque Types in Python
Let's say you're writing a Python library.
In this library, you have some collection of state that represents "options" or "configuration" for a bunch of operations. Such a set of options is a bundle of potentially ever-increasing complexity. Thus, you will want it to have an extremely minimal compatibility surface, with a very carefully chosen public interface, that is either small, or perhaps nothing at all. Such an object conveys state and might have some private behavior, but all you want consumers to be able to do is build it in very constrained, specific ways, and then pass it along as a parameter to your own APIs.
By way of example, imagine that you're wrapping a library that handles shipping physical packages.
There are a zillion ways to do it ship a package. There are different carriers who can ship it for you. There's air freight, and ground freight, and sea freight. There's overnight shipping. There's the option to require a signature. There's package tracking and certified mail. Suffice it to say, lots of stuff.
If you are starting out to implement such a library, you might need an object called something like ShippingOptions that encapsulates some of this. At the core of your library you might have a function like this:
1 2 3 4 5 |
|
If you are starting out implementing such a library, you know that you're going to get the initial implementation of ShippingOptions wrong; or, at the very least, if not "wrong", then "incomplete". You should not want to commit to an expansive public API with a ton of different attributes until you really understand the problem domain pretty well.
Yet, ShippingOptions is absolutely vital to the rest of your library. You'll need to construct it and pass it to various methods like estimateShippingCost and shipPackage. So you're not going to want a ton of complexity and churn as you evolve it to be more complex.
Worse yet, this object has to hold a ton of state. It's got attributes, maybe even quite complex internal attributes that relate to different shipping services.
Right now, today, you need to add something so you can have "no rush", "standard" and "expedited" options. You can't just put off implementing that indefinitely until you can come up with the perfect shape. What to do?
The tool you want here is the opaque data type design pattern. C is lousy with such things (FILE, pthread_*_t, fd_set, etc). A typedef in a header file can easily achieve this.
But in Python, if you expose a dataclass - or any class, really - even if you keep all your fields private, the constructor is still, inherently, public. You can make it raise an exception or something, but your type checker still won't help your users; it'll still look like it's a normal class.
Luckily, Python typing provides a tool for this: typing.NewType.
Let's review our requirements:
- We need a type that our client code can use in its type annotations; it needs to be public.
- They need to be able to consruct it somehow, even if they shouldn't be able to see its attributes or its internal constructor arguments.
- To express high-level things (like "ship fast") that should stay supported as we add more nuanced and complex configurations in the future (like "ship with the fastest possible option provided by the lowest-cost carrier that supports signature verification").
In order to solve these problems respectively, we will use:
- a public
NewType, which gives us our public name... - which wraps a private class with entirely private attributes, to give us an actual data structure, while not exposing the constructor,
- a set of public constructor functions, which returns our
NewType.
When we put that all together, it looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
As a snapshot in time, this is not all that interesting; we could have just exposed _RealShipOpts as a public class and saved ourselves some time. The fact that this exposes a constructor that takes a string is not a big deal for the present moment. For an initial quick and dirty implementation, we can just do checks like if options._speed == "fast" in our shipping and estimation code.
However, the main thing we are doing here is preserving our flexibility to evolve the related APIs into the future, so let's see how we might do that. For example, let's allow the shipping options to contain a concrete and specific carrier and freight method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
As a NewType, our public ShippingOptions type doesn't have a constructor. Since _RealShipOpts is private, and all its attributes are private, we can completely remove the old versions.
Anything within our shipping library can still access the private variables on ShippingOptions; as a NewType, it's the same type as its base at runtime, so it presents minimal1 overhead.
Clients outside our shipping library can still call all of our public constructors: shipFast, shipNormal, and shipSlow all still work with the same (as far as calling code knows) signature and behavior.
If you need to build and convey some state within your public API, while avoiding breakages associated with compatibility churn, hopefully this technique can help you do that!
Acknowledgments
Thanks for reading, and thank you to my patrons who are supporting my writing on this blog. If you like what you've read here and you'd like to read more of it, or you'd like to support my various open-source endeavors, you can support my work as a sponsor.
-
The overhead is minimal, but it is not completely zero. The suggested idiom for converting to a
NewTypeis to call it like a function, as I've done in these examples, but if you are wanting to use this pattern inside of a hot loop, you can use# type: ignore[return-value]comments to avoid that small cost. โฉ
22 May 2026 12:33am GMT
04 Apr 2026
Planet Twisted
Donovan Preston: Using osascript with terminal agents on macOS
Here is a useful trick that is unreasonably effective for simple computer use goals using modern terminal agents. On macOS, there has been a terminal osascript command since the original release of Mac OS X. All you have to do is suggest your agent use it and it can perform any application control action available in any AppleScript dictionary for any Mac app. No MCP set up or tools required at all. Agents are much more adapt at using rod terminal commands, especially ones that haven't changed in 30 years. Having a computer control interface that hasn't changed in 30 years and has extensive examples in the Internet corpus makes modern models understand how to use these tools basically Effortlessly. macOS locks down these permissions pretty heavily nowadays though, so you will have to grant the application control permission to terminal. But once you have done that, the range of possibilities for commanding applications using natural language is quite extensive. Also, for both Safari and chrome on Mac, you are going to want to turn on JavaScript over AppleScript permission. This basically allows claude or another agent to debug your web applications live for you as you are using them.In chrome, go to the view menu, developer submenu, and choose "Allow JavaScript from Apple events". In Safari, it's under the safari menu, settings, developer, "Allow JavaScript from Apple events". Then you can do something like "Hey Claude, would you Please use osascript to navigate the front chrome tab to hacker news". Once you suggest using OSA script in a session it will figure out pretty quickly what it can do with it. Of course you can ask it to do casual things like open your mail app or whatever. Then you can figure out what other things will work like please click around my web app or check the JavaScript Console for errors. Another very important tips for using modern agents is to try to practice using speech to text. I think speaking might be something like five times faster than typing. It takes a lot of time to get used to, especially after a lifetime of programming by typing, but it's a very interesting and a different experience and once you have a lot of practice It starts to to feel effortless.
04 Apr 2026 1:31pm GMT