Planet Python
Last update: February 01, 2026 04:44 PM UTC
February 01, 2026
Graham Dumpleton
Developer Advocacy in 2026
I got into developer advocacy in 2010 at New Relic, followed by a stint at Red Hat. When I moved to VMware, I expected things to continue much as before, but COVID disrupted those plans. When Broadcom acquired VMware, the writing was on the wall and though it took a while, I eventually got made redundant. That was almost 18 months ago. In the time since, I've taken an extended break with overseas travel and thoughts of early retirement. It's been a while therefore since I've done any direct developer advocacy.
One thing became clear during that time. I had no interest in returning to a 9-to-5 programming job in an office, working on some dull internal system. Ideally, I'd have found a company genuinely committed to open source where I could contribute to open source projects. But those opportunities are thin on the ground, and being based in Australia made it worse as such companies are typically in the US or Europe and rarely hire outside their own region.
Recently I've been thinking about getting back into developer advocacy. The job market makes this a difficult proposition though. Companies based in the US and Europe that might otherwise be good places to work tend to ignore the APAC region, and even when they do pay attention, they rarely maintain a local presence. They just send people out when they need to.
Despite the difficulties, I would also need to understand what I was getting myself into. How much had developer advocacy changed since I was doing it? What challenges would I face working in that space?
So I did what any sensible person does in 2026. I asked an AI to help me research the current state of the field. I started with broad questions across different topics, but one question stood out as a interesting starting point: What are the major forces that have reshaped developer advocacy in recent years?
This post looks at what the AI said and how it matched my own impressions.
Catching Up: What's Changed?
The AI came back with three main themes.
Force 1: AI Has Changed Everything
What the AI told me:
The data suggests a fundamental shift in how developers work. Around 84% of developers now use AI tools on a daily basis, with more than half relying on them for core development tasks. Developers are reporting 30-60% time savings on things like boilerplate generation, debugging, documentation lookup, and testing.
This has significant implications for developer advocacy. The traditional path—developer has a problem, searches Google, lands on Stack Overflow or your documentation, reads a tutorial—has been disrupted. Now, developers increasingly turn to AI assistants first. They describe their problem and get an immediate, contextual answer, often with working code included.
What this means is that your content now has two audiences: humans and AI systems. Your documentation isn't just being read by developers—it's being ingested, processed, and used by AI to generate answers. If the AI misrepresents your product or gives incorrect guidance, you may never even know it's happening.
There's even an emerging standard called llms.txt that's gaining traction. First proposed in late 2024, it's a file that tells AI systems what content to prioritise and how to interpret it. By 2026, documentation without this kind of AI-optimisation struggles to surface correctly when developers ask AI assistants for help. It's like SEO, but for language models instead of search engines.
My take:
Yep, no surprise here. I've seen this firsthand with my own mod_wsgi and wrapt open source projects, and it goes beyond what the AI described. In the past, people would ask questions via Stack Overflow, project-specific forums, or mailing lists if they did get stuck. These days, direct engagement is rare. Occasionally someone might raise a problem on GitHub, but those old communication channels have largely disappeared. People using your code just ask the AI instead.
As for the suggestion that I need to put in extra work to help AI systems, such as providing an llms.txt file, I'm not convinced. From what I can see, AI is already doing a good job of working things out on its own. Besides, project documentation, while supposedly authoritative, isn't the only source of information out there. I don't think an AI will give your llms.txt file any more credibility than the third-party content scattered across blogs, conference talks, and elsewhere.
Either way, AI has drastically changed things and now sits between a user and documentation for a project. It can be demoralising to write documentation when you don't know if a real person will ever actually read it anymore.
Force 2: Content Saturation Has Reached Crisis Levels
What the AI told me:
The volume of content being produced has exploded. Current estimates put global data production at over 403 million terabytes daily, and around 80% of developers report experiencing information overload. The symptoms show up in the data: lower click-through rates, shorter time-on-page, faster scrolling behaviour, declining email open rates.
The root cause? AI has made content creation trivially easy. Anyone can generate a tutorial, write a blog post, or produce documentation in minutes. The result is a flood of content, much of it generic, shallow, and nearly indistinguishable from everything else.
Developer communities have adapted. Developers have become sophisticated at filtering low-quality content. Generic "getting started" tutorials are largely ignored because AI can generate them on demand. Surface-level blog posts get scrolled past. The attention bar has risen significantly.
This creates a dual reality. The challenge: your content competes against unprecedented noise. The opportunity: genuinely valuable content stands out more than ever, precisely because it's rare.
My take:
I'd have to agree with the AI here. I'm definitely feeling overwhelmed by the sheer volume of content when researching topics these days. That said, I'd qualify that observation. It's particularly acute for topics with a lot of buzz around them, AI being the obvious example. The same applies when searching for information on large software projects with broad user bases, like popular programming languages or major web frameworks. But for niche areas, I don't see this as much of an issue. People aren't going to generate content for obscure projects just for the sake of it, especially when there's no hype driving attention.
When talking about developer advocacy as a job, the focus is usually a company's own product. For most companies, especially early-stage ones, they don't have the mind share yet, so they fit into that niche category.
That said, content saturation is still a real problem. I'm tired of the endless YouTube videos on the same topics and how self-proclaimed advocates jump on trends to outdo each other. Too often, the focus is just on shipping content, which means discussions end up shallow and repetitive with little substance.
Force 3: Platform Fragmentation Has Disrupted Communities
What the AI told me:
The platform landscape has shifted significantly. When you were last active, Slack dominated developer communities, Discord was primarily for gamers, and Twitter served as the town square for tech discourse.
None of that holds anymore.
Discord has overtaken Slack for developer communities. The drivers were primarily economic—Slack's pricing pushed communities toward free alternatives—and feature-related, with Discord offering better moderation tools, role management, and community features. Many developers who were daily Slack users between 2015-2020 have essentially stopped using Slack and are now primarily active in Discord channels.
The Twitter situation is more fragmented. Some developers moved to Mastodon, others to Bluesky, and many simply reduced their social media engagement altogether. LinkedIn has grown as a platform for technical content. There's no longer a single "town square" where developers reliably gather.
The practical implication: you can't rely on any single platform for community strategy. Presence across multiple spaces, with different approaches for each, is now necessary.
My take:
My age is probably showing here. The AI talks about people moving from Slack to Discord and the demise of Twitter. I still miss mailing lists. Back then, I found the asynchronous nature of mailing lists to be a much better forum for discussions with users. You could take your time understanding questions and drafting thoughtful responses. These days, with real-time discussion platforms, there's pressure to provide immediate answers, which often means less effort goes into truly understanding a user's problem.
To me migrations between platforms for the purpose of providing support to users is inevitable, especially as technology changes. This doesn't mean that new platforms are going to be better though.
Of the disruptions, I felt the demise of Twitter most acutely. It provided more community interactions for me than other discussion forums. When everyone fled Twitter, I lost those connections and don't feel as close to developer communities as I once did, especially the Python community. COVID and the shutdown of conferences during that time compounded this. Overall, I don't feel as connected to the Python community as I was in the past.
Initial Reflections
Having gone through these three forces, I'm left with mixed feelings. Nothing the AI said was really a surprise though.
The main challenge in getting back into developer advocacy is adapting to how AI has changed everything.
I don't see it as insurmountable though, especially since companies expanding their developer advocacy programs are typically niche players without a huge volume of content about their product already out there. The key is ensuring the content they do have on their own site addresses what users need, and expanding from there as necessary.
Relying solely on documentation isn't the answer either. When I've done developer advocacy in the past, I found that online interactive learning platforms could supplement documentation well. That's even more true now, as users aren't willing to spend much time reading through documentation. You need something to hook them, a way to quickly show how your product might help them. Interactive platforms where they can experiment with a product without installing it locally can make a real difference here.
What's Next
Right now I'm not sure what that next step is. I'll almost certainly need to find some sort of job, at least for the next few years before I can think about retiring completely. I still work on my own open source projects, but they don't pay the bills.
One of those projects is actually an interactive learning platform, exactly the sort of thing I've been talking about above. I've invested significant time on it, but it's something I've never really discussed here on my blog. As I think through what comes next, it seems like time to change that.
February 01, 2026 10:38 AM UTC
Tryton News
Tryton News February 2026
During the last month we focused on fixing bugs, improving the behaviour of things, speeding-up performance issues - building on the changes from our release last month. But we also added many new features which we would like to introduce to you in this newsletter.
For an in depth overview of the Tryton issues please take a look at our issue tracker or see the issues and merge requests filtered by label.
Changes for the User
Sales, Purchases and Projects
We now add the optional gift card field to the list of products. This helps to search gift card products.
Now we clean the former quotation_date when copy sale records as we already do with the sale_date.
We now display the origin field of requests in the purchase request list. When the purchase request is not from a stock supply, it is useful for the user who takes action, to know the origin of the request.
Accounting, Invoicing and Payments
Now we support allowance and charge in UBL invoices.
We now fill the buyer’s item identification (BuyersItemIdentification) in the UBL invoice, when sale product customer is activated.
On the invoice line we have properties like product_name to get related supplier and customer codes.
Now we add a cron scheduler to reconcile account move lines. On larger setups the number of accounts and parties to reconcile can be very numerous. It would consume too much time to execute the reconciliation wizard, even with the automatic option.
In this cases it will be better to run the reconciliation process as a scheduled task in background.
We now add support for payment references for incoming invoice. As the invoice manages payment references, we support to fill it using information from the incoming document.
Now Tryton warns the user before creating an overpayment. Sometimes users book a payment directly as a move line but without creating a payment record. If the line is not yet reconciled (it can be a partial payment), the line to pay stand still there showing the full amount to pay. This can lead to over pay a party without any notice for the user.
So we now ensure that the amount being paid does not exceed the payable (or receivable) amount of the party.
There is no guarantee against overpayment The proper way to avoid is to always use the payments functionality. But the warning will catch most of the mistakes.
Now we add support for Peppyrus webhooks in Tryton’s document incoming functionality.
We now set Belgian account 488 as deposit.
Now we add cy_vat as tax identifier type.
Stock, Production and Shipments
We now store the original planned date of requested internal shipments and productions.
For shipments created by sales we already store the original planned date to compute the delay. Now we do the same for the supplied shipments and productions.
Now we use a fields.Many2One to display either the product or the variant in the stock reporting instead of the former reference field. With this change the user is able to search for product or variant specific attributes. But the reference field is still useful to build the domain, so we keep it invisible.
We now add routings on BOM form to ease the setup.
Now we use the default warehouse when creating new product locations.
User Interface
Now we allow to reorder tabs in Sao, the Tryton web client.
Now we use the default digit value to calculate the width of the float widget in Sao.
New Releases
We released bug fixes for the currently maintained long term support series
7.0 and 6.0, and for the penultimate series 7.8 and 7.6.
Security
Please update your systems to take care of a security related bug we found last month.Mahdi Afshar and Abdulfatah Abdillahi have found that trytond sends the trace-back to the clients for unexpected errors. This trace-back may leak information about the server setup. Impact CVSS v3.0 Base Score: 4.3 Attack Vector: Network Attack Complexity: Low Privileges Required: Low User Interaction: None Scope: Unchanged Confidentiality: Low Integrity: None Availability: None Workaround A possible workaround is to configure an error handler which would remove the trace-back from the respo…
Abdulfatah Abdillahi has found that sao does not escape the completion values. The content of completion is generally the record name which may be edited in many ways depending on the model. The content may include some JavaScript which is executed in the same context as sao which gives access to sensitive data such as the session. Impact CVSS v3.0 Base Score: 7.3 Attack Vector: Network Attack Complexity: Low Privileges Required: Low User Interaction: Required Scope: Unchanged Confidentiality…
Mahdi Afshar has found that trytond does not enforce access rights for the route of the HTML editor (since version 6.0). Impact CVSS v3.0 Base Score: 7.1 Attack Vector: Network Attack Complexity: Low Privileges Required: Low User Interaction: None Scope: Unchanged Confidentiality: High Integrity: Low Availability: None Workaround A possible workaround is to block access to the html editor. Resolution All affected users should upgrade trytond to the latest version. Affected versions per ser…
Cédric Krier has found that trytond does not enforce access rights for data export (since version 6.0). Impact CVSS v3.0 Base Score: 6.5 Attack Vector: Network Attack Complexity: Low Privileges Required: Low User Interaction: None Scope: Unchanged Confidentiality: High Integrity: None Availability: None Workaround There is no workaround. Resolution All affected users should upgrade trytond to the latest version. Affected versions per series: trytond: 7.6: <= 7.6.10 7.4: <= 7.4.20 7.0: <=…
Changes for the System Administrator
Now we allow filtering users to be notified by cron tasks. When notifying subscribing users of a cron task, messages may make sense only for some user. For example if the message is about a specific company, we want to notify only the users having access to this company.
We now dump the action value of cron notification as JSON if it is not a string (aka JSON).
Now we log an exception when the Binary field retrieval of a file ID from the file-store fails.
We now support 0 as parameter of max_tasks_per_child.
The ProcessPoolExecutor requires a positive number or None. But when using an environment variable to configure a startup script, it is complicated to set no value (which means skip the argument). Now it is easier because 0 is considered as None.
Changes for Implementers and Developers
We now log an exception when it fails to open the XML-file of a view (view arch).
Now we format dates used as record names with the contextual language.
We now add the general test PartyCheckReplaceMixin to check replaced fields of the replace party wizard.
1 post - 1 participant
February 01, 2026 07:00 AM UTC
January 31, 2026
EuroPython
Humans of EuroPython: Naa Ashiorkor Nortey
Behind every inspiring talk, networking session, and workshop at EuroPython lies countless hours of dedication from our amazing volunteers. From organizing logistics and securing speakers to welcoming attendees, these passionate community members make our conference possible year after year. Without their selfless commitment and hard work, EuroPython simply wouldn&apost exist.
Here’s our recent conversation with Naa Ashiorkor Nortey, who led the EuroPython 2025 Speaker Mentorship Team, contributed to the Programme Team and mentored at the Humble Data workshop.
We appreciate your work on the conference, Naa!
EP: Had you attended EuroPython before volunteering, or was volunteering your first experience with it?
My first experience volunteering at EuroPython was in 2023. I volunteered at the registration desk and as a session chair, and I’m still here volunteering.
EP: What&aposs one task you handled that attendees might not realize happens behind the scenes at EuroPython?
I can’t think of a specific task, but I would say that some attendees might not realise the number of hours volunteers put in for EuroPython. Usually, a form might be filled out with the number of hours a volunteer can dedicate in a week, but in reality the number of hours invested might be way more than that. There are volunteers in different time zones with different personal lives, so imagine making all that work.
EP: Was there a moment when you felt your contribution really made a difference?
Generally, showing up at the venue after months of planning, it just hit me how much difference my contribution makes. Specifically at EuroPython 2025, where I had the opportunity to lead the Speaker Mentorship Team. I interviewed one of the mentees during the conference. She mentioned that it was her first time speaking and highlighted how the speaker mentorship programme and her mentor greatly impacted her. At that moment, I felt my contribution really made a difference.
EP: What surprised you most about the volunteer experience?
The dedication and commitment of some of the volunteers were so inspiring.
EP: If you could describe the volunteer experience in three words, what would they be?
Fun learning experience.
EP: Do you have any tips for first-time EuroPython volunteers?
Don’t be afraid to volunteer, even if it involves leading one of the teams or contributing to a team you have no experience with. You can learn the skills needed in the team while volunteering. Everyone is supportive and ready to help. Communicate as much as you can and enjoy the experience.
EP: Thank you for the interview, Naa!
Python⇒Speed
Speeding up NumPy with parallelism
If your NumPy code is too slow, what next?
One option is taking advantage of the multiple cores on your CPU: using a thread pool to do work in parallel. Another option is to tune your code so it’s less wasteful. Or, since these are two different sources of speed, you can do both.
In this article I’ll cover:
- A simple example of making a NumPy algorithm parallel.
- A separate kind of optimization, making a more efficient implementation in Numba.
- How to get even more speed by using both at once.
- Aside: A hardware limit on parallelism.
- Aside: Why not Numba’s built-in parallelism?
Armin Ronacher
Pi: The Minimal Agent Within OpenClaw
If you haven’t been living under a rock, you will have noticed this week that a project of my friend Peter went viral on the internet. It went by many names. The most recent one is OpenClaw but in the news you might have encountered it as ClawdBot or MoltBot depending on when you read about it. It is an agent connected to a communication channel of your choice that just runs code.
What you might be less familiar with is that what’s under the hood of OpenClaw is a little coding agent called Pi. And Pi happens to be, at this point, the coding agent that I use almost exclusively. Over the last few weeks I became more and more of a shill for the little agent. After I gave a talk on this recently, I realized that I did not actually write about Pi on this blog yet, so I feel like I might want to give some context on why I’m obsessed with it, and how it relates to OpenClaw.
Pi is written by Mario Zechner and unlike Peter, who aims for “sci-fi with a touch of madness,” 1 Mario is very grounded. Despite the differences in approach, both OpenClaw and Pi follow the same idea: LLMs are really good at writing and running code, so embrace this. In some ways I think that’s not an accident because Peter got me and Mario hooked on this idea, and agents last year.
What is Pi?
So Pi is a coding agent. And there are many coding agents. Really, I think you can pick effectively anyone off the shelf at this point and you will be able to experience what it’s like to do agentic programming. In reviews on this blog I’ve positively talked about AMP and one of the reasons I resonated so much with AMP is that it really felt like it was a product built by people who got both addicted to agentic programming but also had tried a few different things to see which ones work and not just to build a fancy UI around it.
Pi is interesting to me because of two main reasons:
- First of all, it has a tiny core. It has the shortest system prompt of any agent that I’m aware of and it only has four tools: Read, Write, Edit, Bash.
- The second thing is that it makes up for its tiny core by providing an extension system that also allows extensions to persist state into sessions, which is incredibly powerful.
And a little bonus: Pi itself is written like excellent software. It doesn’t flicker, it doesn’t consume a lot of memory, it doesn’t randomly break, it is very reliable and it is written by someone who takes great care of what goes into the software.
Pi also is a collection of little components that you can build your own agent on top. That’s how OpenClaw is built, and that’s also how I built my own little Telegram bot and how Mario built his mom. If you want to build your own agent, connected to something, Pi when pointed to itself and mom, will conjure one up for you.
What’s Not In Pi
And in order to understand what’s in Pi, it’s even more important to understand what’s not in Pi, why it’s not in Pi and more importantly: why it won’t be in Pi. The most obvious omission is support for MCP. There is no MCP support in it. While you could build an extension for it, you can also do what OpenClaw does to support MCP which is to use mcporter. mcporter exposes MCP calls via a CLI interface or TypeScript bindings and maybe your agent can do something with it. Or not, I don’t know :)
And this is not a lazy omission. This is from the philosophy of how Pi works. Pi’s entire idea is that if you want the agent to do something that it doesn’t do yet, you don’t go and download an extension or a skill or something like this. You ask the agent to extend itself. It celebrates the idea of code writing and running code.
That’s not to say that you cannot download extensions. It is very much supported. But instead of necessarily encouraging you to download someone else’s extension, you can also point your agent to an already existing extension, say like, build it like the thing you see over there, but make these changes to it that you like.
Agents Built for Agents Building Agents
When you look at what Pi and by extension OpenClaw are doing, there is an example of software that is malleable like clay. And this sets certain requirements for the underlying architecture of it that are actually in many ways setting certain constraints on the system that really need to go into the core design.
So for instance, Pi’s underlying AI SDK is written so that a session can really contain many different messages from many different model providers. It recognizes that the portability of sessions is somewhat limited between model providers and so it doesn’t lean in too much into any model-provider-specific feature set that cannot be transferred to another.
The second is that in addition to the model messages it maintains custom messages in the session files which can be used by extensions to store state or by the system itself to maintain information that either not at all is sent to the AI or only parts of it.
Because this system exists and extension state can also be persisted to disk, it has built-in hot reloading so that the agent can write code, reload, test it and go in a loop until your extension actually is functional. It also ships with documentation and examples that the agent itself can use to extend itself. Even better: sessions in Pi are trees. You can branch and navigate within a session which opens up all kinds of interesting opportunities such as enabling workflows for making a side-quest to fix a broken agent tool without wasting context in the main session. After the tool is fixed, I can rewind the session back to earlier and Pi summarizes what has happened on the other branch.
This all matters because for instance if you consider how MCP works, on most model providers, tools for MCP, like any tool for the LLM, need to be loaded into the system context or the tool section thereof on session start. That makes it very hard to impossible to fully reload what tools can do without trashing the complete cache or confusing the AI about how prior invocations work differently.
Tools Outside The Context
An extension in Pi can register a tool to be available to the LLM to call and every once in a while I find this useful. For instance, despite my criticism of how Beads is implemented, I do think that giving an agent access to a to-do list is a very useful thing. And I do use an agent-specific issue tracker that works locally that I had my agent build itself. And because I wanted the agent to also manage to-dos, in this particular case I decided to give it a tool rather than a CLI. It felt appropriate for the scope of the problem and it is currently the only additional tool that I’m loading into my context.
But for the most part all of what I’m adding to my agent are either skills or TUI extensions to make working with the agent more enjoyable for me. Beyond slash commands, Pi extensions can render custom TUI components directly in the terminal: spinners, progress bars, interactive file pickers, data tables, preview panes. The TUI is flexible enough that Mario proved you can run Doom in it. Not practical, but if you can run Doom, you can certainly build a useful dashboard or debugging interface.
I want to highlight some of my extensions to give you an idea of what’s possible. While you can use them unmodified, the whole idea really is that you point your agent to one and remix it to your heart’s content.
/answer
I don’t use plan mode. I encourage the agent to ask questions and there’s a productive back and forth. But I don’t like structured question dialogs that happen if you give the agent a question tool. I prefer the agent’s natural prose with explanations and diagrams interspersed.
The problem: answering questions inline gets messy. So /answer reads the
agent’s last response, extracts all the questions, and reformats them into a
nice input box.
/todos
Even though I criticize Beads for its
implementation, giving an agent a to-do list is genuinely useful. The /todos
command brings up all items stored in .pi/todos as markdown files. Both the
agent and I can manipulate them, and sessions can claim tasks to mark them as in
progress.
/review
As more code is written by agents, it makes little sense to throw unfinished work at humans before an agent has reviewed it first. Because Pi sessions are trees, I can branch into a fresh review context, get findings, then bring fixes back to the main session.
The UI is modeled after Codex which provides easy to review commits, diffs, uncommitted changes, or remote PRs. The prompt pays attention to things I care about so I get the call-outs I want (eg: I ask it to call out newly added dependencies.)
/control
An extension I experiment with but don’t actively use. It lets one Pi agent send prompts to another. It is a simple multi-agent system without complex orchestration which is useful for experimentation.
/files
Lists all files changed or referenced in the session. You can reveal them in
Finder, diff in VS Code, quick-look them, or reference them in your prompt.
shift+ctrl+r quick-looks the most recently mentioned file which is handy when
the agent produces a PDF.
Others have built extensions too: Nico’s subagent extension and interactive-shell which lets Pi autonomously run interactive CLIs in an observable TUI overlay.
Software Building Software
These are all just ideas of what you can do with your agent. The point of it mostly is that none of this was written by me, it was created by the agent to my specifications. I told Pi to make an extension and it did. There is no MCP, there are no community skills, nothing. Don’t get me wrong, I use tons of skills. But they are hand-crafted by my clanker and not downloaded from anywhere. For instance I fully replaced all my CLIs or MCPs for browser automation with a skill that just uses CDP. Not because the alternatives don’t work, or are bad, but because this is just easy and natural. The agent maintains its own functionality.
My agent has quite a few
skills and crucially
I throw skills away if I don’t need them. I for instance gave it a skill to
read Pi sessions that other engineers shared, which helps with code review. Or
I have a skill to help the agent craft the commit messages and commit behavior I
want, and how to update changelogs. These were originally slash commands, but
I’m currently migrating them to skills to see if this works equally well. I
also have a skill that hopefully helps Pi use uv rather than pip, but I also
added a custom extension to intercept calls to pip and python to redirect
them to uv instead.
Part of the fascination that working with a minimal agent like Pi gave me is that it makes you live that idea of using software that builds more software. That taken to the extreme is when you remove the UI and output and connect it to your chat. That’s what OpenClaw does and given its tremendous growth, I really feel more and more that this is going to become our future in one way or another.
January 30, 2026
Kevin Renskers
Django's test runner is underrated
Every podcast, blog post, Reddit thread, and every conference talk seems to agree: “just use pytest”. Real Python says most developers prefer it. Brian Okken’s popular book calls it “undeniably the best choice”. It’s treated like a rite of passage for Python developers: at some point you’re supposed to graduate from the standard library to the “real” testing framework.
I never made that switch for my Django projects. And after years of building and maintaining Django applications, I still don’t feel like I’m missing out.
What I actually want from tests
Before we get into frameworks, let me be clear about what I need from a test suite:
-
Readable failures. When something breaks, I want to understand why in seconds, not minutes.
-
Predictable setup. I want to know exactly what state my tests are running against.
-
Minimal magic. The less indirection between my test code and what’s actually happening, the better.
-
Easy onboarding. New team members should be able to write tests on day one without learning a new paradigm.
Django’s built-in test framework delivers all of this. And honestly? That’s enough for most projects.
Django tests are just Python’s unittest
Here’s something that surprises a lot of developers: Django’s test framework isn’t some exotic Django-specific system. Under the hood, it’s Python’s standard unittest module with a thin integration layer on top.
TestCase extends unittest.TestCase. The assertEqual, assertRaises, and other assertion methods? Straight from the standard library. Test discovery, setup and teardown, skip decorators? All standard unittest behavior.
What Django adds is integration: Database setup and teardown, the HTTP client, mail outbox, settings overrides.
This means when you choose Django’s test framework, you’re choosing Python’s defaults plus Django glue. When you choose pytest with pytest-django, you’re replacing the assertion style, the runner, and the mental model, then re-adding Django integration on top.
Neither approach is wrong. But it’s objectively more layers.
The self.assert* complaint
A common argument I hear against unittest-style tests is: “I can’t remember all those assertion methods”. But let’s be honest. We’re not writing tests in Notepad in 2026. Every editor has autocomplete. Type self.assert and pick from the list.
And in practice, how many assertion methods do you actually use? In my tests, it’s mostly assertEqual and assertRaises. Maybe assertTrue, assertFalse, and assertIn once in a while. That’s not a cognitive burden.
Here’s the same test in both styles:
# Django / unittest
self.assertEqual(total, 42)
with self.assertRaises(ValidationError):
obj.full_clean()
# pytest
assert total == 42
with pytest.raises(ValidationError):
obj.full_clean()
Yes, pytest’s assert is shorter. It’s a bit easier on the eyes. And I’ll be honest: pytest’s failure messages are better too. When an assertion fails, pytest shows you exactly what values differed with nice diffs. That’s genuinely useful.
But here’s what makes that work: pytest rewrites your code. It hooks into Python’s AST and transforms your test files before they run so it can produce those detailed failure messages from plain assert statements. That’s not necessarily bad - it’s been battle-tested for over a decade. But it is a layer of transformation between what you write and what executes, and I prefer to avoid magic when I can.
For me, unittest’s failure messages are good enough. When assertEqual fails, it tells me what it expected and what it got. That’s usually all I need. Better failure messages are nice, but they’re not worth adding dependencies and an abstraction layer for.
The missing piece: parametrized tests
If there’s one pytest feature people genuinely miss when using Django’s test framework, it’s parametrization. Writing the same test multiple times with different inputs feels wasteful.
But you really don’t need to switch to pytest just for that. The parameterized package solves this cleanly:
from django.test import SimpleTestCase
from parameterized import parameterized
class SlugifyTests(SimpleTestCase):
@parameterized.expand([
("Hello world", "hello-world"),
("Django's test runner", "djangos-test-runner"),
(" trim ", "trim"),
])
def test_slugify(self, input_text, expected):
self.assertEqual(slugify(input_text), expected)
Compare that to pytest:
import pytest
@pytest.mark.parametrize("input_text,expected", [
("Hello world", "hello-world"),
("Django's test runner", "djangos-test-runner"),
(" trim ", "trim"),
])
def test_slugify(input_text, expected):
assert slugify(input_text) == expected
Both are readable. Both work well. The difference is that parameterized is a tiny, focused library that does one thing. It doesn’t replace your test runner, introduce a new fixture system, or bring an ecosystem of plugins. It’s a decorator, not a paradigm shift.
Once I added parameterized, I realized pytest no longer solved a problem I actually had.
Side by side: common test patterns
Let’s look at how typical Django tests compare to pytest’s approach.
Database tests
# Django
from django.test import TestCase
from myapp.models import Article
class ArticleTests(TestCase):
def test_article_str(self):
article = Article.objects.create(title="Hello")
self.assertEqual(str(article), "Hello")
# pytest + pytest-django
import pytest
from myapp.models import Article
@pytest.mark.django_db
def test_article_str():
article = Article.objects.create(title="Hello")
assert str(article) == "Hello"
With Django, database access simply works. TestCase wraps every test in a transaction and rolls it back afterward, giving you a clean slate without extra decorators. pytest-django takes the opposite approach: database access is opt-in. Different philosophies, but I find theirs annoying since most of my tests touch the database anyway, so I’d end up with @pytest.mark.django_db on almost every test.
View tests
# Django
from django.test import TestCase
from django.urls import reverse
class ViewTests(TestCase):
def test_home_page(self):
response = self.client.get(reverse("home"))
self.assertEqual(response.status_code, 200)
# pytest + pytest-django
from django.urls import reverse
def test_home_page(client):
response = client.get(reverse("home"))
assert response.status_code == 200
In Django, self.client is right there on the test class. If you want to know where it comes from, follow the inheritance tree to TestCase. In pytest, client appears because you named your parameter client. That’s how fixtures work: injection happens by naming convention. If you didn’t know that, the code would be puzzling. And if you want to find where a fixture is defined, you might be hunting through conftest.py files across multiple directory levels.
What about fixtures?
Pytest’s fixture system is the other big feature people bring up. Fixtures compose, they handle setup and teardown automatically, and they can be scoped to function, class, module, or session.
But the mechanism is implicit. You’ve already seen the implicit injection in the view test example: name a parameter client and it appears, add db to your function signature and you get database access. Powerful, but also magic you need to learn.
For most Django tests, you need some objects in the database before your test runs. Django gives you two ways to do this:
setUp()runs before each test methodsetUpTestData()runs once per test class, which is faster for read-only data
class ArticleTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.author = User.objects.create(username="kevin")
def test_article_creation(self):
article = Article.objects.create(title="Hello", author=self.author)
self.assertEqual(article.author.username, "kevin")
If you need more sophisticated object creation, factory-boy works great with either framework.
The fixture system solves a real problem - complex cross-cutting setup that needs to be shared and composed. My projects just haven’t needed that level of sophistication. And I’d rather not add the indirection until I do.
The hidden cost of flexibility
Pytest’s flexibility is a feature. It’s also a liability.
In small projects, pytest feels lightweight. But as projects grow, that flexibility can accumulate into complexity. Your conftest.py starts small, then grows into its own mini-framework. You add pytest-xdist for parallel tests (Django has --parallel built-in). You write custom fixtures for DRF’s APIClient (Django’s APITestCase just works). You add a plugin for coverage, another for benchmarking. Each one makes sense in isolation.
Then a test fails in CI but not locally, and you’re debugging the interaction between three plugins and a fixture that depends on two other fixtures.
Django’s test framework doesn’t have this problem because it doesn’t have this flexibility. There’s one way to set up test data. There’s one test client. There’s one way to run tests in parallel. Boring, but predictable.
When I’m debugging a test failure, I want to debug my code, not my test infrastructure.
When I would recommend pytest
I’m not anti-pytest. If your team already has deep pytest expertise and established patterns, switching to Django’s runner would be a net negative. Switching costs are real. If I join a project that uses pytest? I use pytest. This is a preference for new projects, not a religion.
It’s also worth noting that pytest can run unittest-style tests without modification. You don’t have to rewrite everything if you want to try it. That’s a genuinely nice feature.
But if you’re starting fresh, or you’re the one making the decision? Make it a conscious choice. “Everyone uses pytest” can be a valid consideration, but it shouldn’t be the whole argument.
My rule of thumb
Start with Django’s test runner. It’s boring, it’s stable, and it works.
Add parameterized when you need parametrized tests.
Switch to pytest only when you can name the specific problem Django’s framework can’t solve. Not because a podcast told you to, but because you’ve hit an actual wall.
I’ve been building Django applications for a long time. I’ve tried both approaches. And I keep choosing boring.
Boring is a feature in test infrastructure.
The Python Coding Stack
Planning Meals, Weekly Shop, Alternative Constructors Using Class Methods
I’m sure we’re not the only family with this problem: deciding what meals to cook throughout the week. There seems to be just one dish that everyone loves, but we can hardly eat the same dish every day.
So we came up with a system, and I’m writing a Python program to implement it. We keep a list of meals we try out. Each family member assigns a score to each meal. Every Saturday, before we go to the supermarket for the weekly shop, we plan which meals we’ll cook on each day of the week. It’s not based solely on the preference ratings, of course, since my wife and I have the final say to ensure a good balance. Finally, the program provides us with the shopping list with the ingredients we need for all the week’s meals.
I know, we’ve reinvented the wheel. There are countless apps that do this. But the fun is in writing your own code to do exactly what you want.
I want to keep this article focussed on just one thing: alternative constructors using class methods. Therefore, I won’t go through the whole code in this post. Perhaps I’ll write about the full project in a future article.
So, here’s what you need to know to get our discussion started.
Do you learn best from one-to-one sessions? The Python Coding Place offers one-to-one lessons on Zoom. Try them out, we bet you’ll love them. Find out more about one-to-one private sessions.
Setting the Scene • Outlining the Meal and WeeklyMealPlanner Classes
Let me outline two of the classes in my code. The first is the Meal class. This class – you guessed it – deals with each meal. Here’s the class’s .__init__() method:
The meal has a name so we can easily refer to it, so there’s a .name data attribute. And the meals I cook are different from the meals my wife cooks, which is why there’s a .person_cooking data attribute. This matters as on some days of the week, only one of us is available to prepare dinner, so this attribute becomes relevant!
There are also days when we have busy afternoons and evenings with children’s activities, so we need to cook a quick meal. The .quick_meal data attribute is a Boolean flag to help with planning for these hectic days.
Then there’s the .ingredients data attribute. You don’t need me to explain this one. And since each family member rates each meal, there’s a .ratings dictionary to keep track of the scores.
The class has more methods, such as add_ingredient(), remove_ingredient(), add_rating(), and more. There’s also code to save to and load from CSV and JSON files. But these are not necessary for today’s article, so I’ll leave them out.
There’s also a WeeklyMealPlanner class:
The ._meals data attribute is a dictionary with the days of the week as keys and Meal instances as values. It’s defined as a non-public attribute to be used with the read-only property .meals. The .meals property returns a shallow copy of the ._meals dictionary. This makes it safer as it’s harder for a user to make changes directly to this dictionary. The dictionary is modified only through methods within WeeklyMealPlanner. I’ve omitted the rest of the methods in this class as they’re not needed for this article.
You can read more about properties in Python in this article: The Properties of Python’s ‘property’
So, each time we try a new dish, we create a Meal object, and each family member rates it. This meal then goes into our collection of meals to choose from each week. On Saturday, we choose the meals we want for the week, put them in a WeeklyMealPlanner instance, and we’re almost ready to go…
At the Supermarket
Well, we’re almost ready to go to the supermarket at this point. So, here’s another class:
A ShoppingList object has an .ingredients data attribute. This attribute is a dictionary. The keys are the ingredients, and the values are the quantities needed for each ingredient. I’m also showing the .add_ingredient() method, which I’ll need later on. So, you can create an instance of ShoppingList in the usual way:
Then, you can add ingredients as needed. But this is annoying for us on a Saturday. Here’s why…
Do you want to master Python one article at a time? Then don’t miss out on the article in The Club which are exclusive to premium subscribers here on The Python Coding Stack
Alternative Constructor
Before describing our Saturday problems, let’s briefly revisit what happens when you create an instance of a class. When you place parentheses after the class name, Python does two things: it creates a blank new object, and it initialises it. The creation of the new object almost always happens “behind the scenes”. The .__new__() method creates a new object, but you rarely need to override it. And the .__init__() method performs the object’s initialisation.
You can only have one .__init__() special method in a class. Does this mean there’s only one way to create an instance of a class?
Not quite, no. Although there’s no way to define more .__init__() methods, there are ways to create instances through different routes. The @singledispatchmethod decorator is a useful tool, but one I’ll discuss in a future post. Today, I want to talk about using class methods as alternative constructors.
Back to a typical Saturday in our household. We just finished choosing the seven dinners we plan to have this coming week, and we created a WeeklyMealPlanner instance. So we should now create a ShoppingList instance using ShoppingList() and then go through all the meals we chose, entering their ingredients.
Wouldn’t it be nice if we could just create a ShoppingList instance directly from the WeeklyMealPlanner instance? But that would require a different way to create an instance of ShoppingList.
Let’s define an alternative constructor, then:
There’s a new method called .from_meal_planner(). However, this is not an instance method. It doesn’t belong to an instance of the class. Instead, it belongs to the class directly. The @classmethod decorator tells Python to treat this method as a class method. Note that the first parameter in this method is not self, as with the usual (instance) methods. Instead, you use cls, which is the parameter name used by convention to refer to the class.
Whereas self in an instance method represents the instance of a class, cls represents the class directly. So, unlike instance methods, class methods don’t have access to the instance. Therefore, class methods don’t have access to instance attributes.
The first line of this method creates an instance of the class. Look at the expression cls(), which comes after the = operator. Recall that cls refers to the class. So, cls is the same as ShoppingList in this example. But adding parentheses after the class creates an instance. You assign this new instance to the local variable shopping_list. You use cls rather than ShoppingList to make the class more robust in case you choose to subclass it later.
Fast-forward to the end of this class method, and you’ll see that the method returns this new instance, shopping_list. However, it makes changes to the instance before returning it. The method fetches all the ingredients from each meal in the WeeklyMealPlanner instance and populates the .ingredients data attribute in the new ShoppingList instance.
In summary, the class method doesn’t have access to an instance through the self parameter. But since it has access to the class, the method uses the class to create a new instance and initialise it, adding steps to the standard .__init__() method.
Therefore, this class method creates and returns an instance of ShoppingList with its .ingredients data attribute populated with the ingredients you need for all the meals in the week.
You now have an alternative way of creating an instance of ShoppingList:
This class now has two ways to create instances. The standard one using ShoppingList() and the alternative one using ShoppingList.from_meal_planner(). It’s common for class methods used as alternative constructors to have names starting with from_*.
You can have as many alternative constructors as you need in a class.
Question: if it’s more useful to create a shopping list directly from the weekly meal planner, couldn’t you implement this logic directly in the .__init__() method? Yes, you could. But this would create a tight coupling between the two classes, ShoppingList and WeeklyMealPlanner. You can no longer use ShoppingList without an instance of WeeklyMealPlanner, and you can no longer easily create a blank ShoppingList instance.
Creating two constructors gives you the best of both worlds. ShoppingList is still flexible enough so you can use it as a standalone class or in conjunction with other classes in other projects. But you also have access to the alternative constructor ShoppingList.from_meal_planner() when you need it.
Alternative Constructors in the Wild
You may have already seen and used alternative constructors, perhaps without noticing.
Let’s consider dictionaries. The standard constructor is dict() – the name of the class followed by parentheses. As it happens, you have several options when using dict() – you can pass a mapping, or an iterable of pairs, or **kwargs. You can read more about these alternatives in this article: dict() is More Versatile Than You May Think.
But there’s another alternative constructor that doesn’t use the standard constructor dict() but still creates a dictionary. This is dict.fromkeys():
You can have a look at help(dict.fromkeys). You’ll see the documentation text refer to this method as a class method, just like the ShoppingList.from_meal_planner() class method you defined earlier.
And if you use the datetime module, you most certainly have used alternative constructors using class methods. The standard constructor when creating a datetime.datetime instance is the following:
However, there are several class methods you can use as alternative constructors:
Have a look at other datetime.datetime methods starting with from_.
Your call…
The Python Coding Place offers something for everyone:
• a super-personalised one-to-one 6-month mentoring option
$ 4,750
• individual one-to-one sessions
$ 125
• a self-led route with access to 60+ hrs of exceptional video courses and a support forum
$ 400
Final Words
Python restricts you to defining only one .__init__() method. But there are still ways for you to create instances of a class through different routes. Class methods are a common way of creating alternative constructors for a class. You call them directly through the class and not through an instance of the class – ShoppingList.from_meal_planner(). The class method then creates an instance, modifies it as needed, and finally returns the customised instance.
Now, let me see what’s on tonight’s meal planner and, more importantly, whether it’s my turn to cook.
Code in this article uses Python 3.14
The code images used in this article are created using Snappify. [Affiliate link]
Join The Club, the exclusive area for paid subscribers for more Python posts, videos, a members’ forum, and more.
You can also support this publication by making a one-off contribution of any amount you wish.
For more Python resources, you can also visit Real Python—you may even stumble on one of my own articles or courses there!
Also, are you interested in technical writing? You’d like to make your own writing more narrative, more engaging, more memorable? Have a look at Breaking the Rules.
And you can find out more about me at stephengruppetta.com
Further reading related to this article’s topic:
Appendix: Code Blocks
Code Block #1
class Meal:
def __init__(
self,
name,
person_cooking,
quick_meal=False,
):
self.name = name
self.person_cooking = person_cooking
self.quick_meal = quick_meal
self.ingredients = {} # ingredient: quantity
self.ratings = {} # person: rating
# ... more methods
Code Block #2
class WeeklyMealPlanner:
def __init__(self):
self._meals = {} # day: Meal
@property
def meals(self):
return dict(self._meals)
# ... more methods
Code Block #3
class ShoppingList:
def __init__(self):
self.ingredients = {} # ingredient: quantity
def add_ingredient(self, ingredient, quantity=1):
if ingredient in self.ingredients:
self.ingredients[ingredient] += quantity
else:
self.ingredients[ingredient] = quantity
# ... more methods
Code Block #4
ShoppingList()
Code Block #5
class ShoppingList:
def __init__(self):
self.ingredients = {} # ingredient: quantity
@classmethod
def from_meal_planner(cls, meal_planner: WeeklyMealPlanner):
shopping_list = cls()
for meal in meal_planner.meals.values():
if meal is None:
continue
for ingredient, quantity in meal.ingredients.items():
shopping_list.add_ingredient(ingredient, quantity)
return shopping_list
def add_ingredient(self, ingredient, quantity=1):
if ingredient in self.ingredients:
self.ingredients[ingredient] += quantity
else:
self.ingredients[ingredient] = quantity
Code Block #6
# if my_weekly_planner is an instance of 'WeeklyMealPlanner', then...
shopping_list = ShoppingList.from_meal_planner(my_weekly_planner)
Code Block #7
dict.fromkeys(["James", "Bob", "Mary", "Jane"])
# {'James': None, 'Bob': None, 'Mary': None, 'Jane': None}
Code Block #8
import datetime
datetime.datetime(2026, 1, 30)
# datetime.datetime(2026, 1, 30, 0, 0)
Code Block #9
datetime.datetime.today()
# datetime.datetime(2026, 1, 30, 12, 54, 2, 243976)
datetime.datetime.fromisoformat("2026-01-30")
# datetime.datetime(2026, 1, 30, 0, 0)
For more Python resources, you can also visit Real Python—you may even stumble on one of my own articles or courses there!
Also, are you interested in technical writing? You’d like to make your own writing more narrative, more engaging, more memorable? Have a look at Breaking the Rules.
And you can find out more about me at stephengruppetta.com
Real Python
The Real Python Podcast – Episode #282: Testing Python Code for Scalability & What's New in pandas 3.0
How do you create automated tests to check your code for degraded performance as data sizes increase? What are the new features in pandas 3.0? Christopher Trudeau is back on the show this week with another batch of PyCoder's Weekly articles and projects.
[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
January 29, 2026
PyBites
7 Software Engineering Fixes To Advance As A Developer
It’s January! If you look back at yourself from exactly one year ago, January 2025, how different are you as a developer from then to now?
Did you ship the app you were thinking about? Did you finally learn how to configure a proper CI/CD pipeline? Did you land the Senior role you were after?
Or did you just watch a lot more YouTube videos and buy a few more Udemy courses that you haven’t finished yet?
If the answer stings a little bit, you aren’t alone.
Over the last six years of coaching hundreds of developers in PDM, Bob and I have noticed a pattern. We see the same specific bottlenecks that keep smart, capable people stuck in Tutorial Hell for years.
They know the syntax and can solve code challenges, but they aren’t shipping.
In this week’s episode of the Pybites Podcast, we get straight to the fix. We aren’t talking about the latest Python library or a cool new feature in Django. We’re talking about the 7 Engineering Shifts you need to make to stop going in circles and actually become a professional software engineer this year.
We dive deep into the hard truths, including:
- The “Graveyard of Abandoned Repos”: Why having 10 half-finished toy apps on your GitHub is actually hurting your career, and why finishing one boring project is worth more than starting five exciting ones.
- Scripts vs. Systems: Why knowing Python is only 30% of the job, and why you are likely failing because you’re ignoring the ecosystem (Docker, Git, Testing, CI/CD).
- The “Vacuum” Trap: Why coding alone is the fastest way to bake technical debt into your brain, and how to get the feedback loop you actually need.
- Debugging Your Mindset: How to use the 20-Minute Rule to stop wasting hours on a single bug without giving up.
We are sharing the exact tips we give our PDM coaching clients to get them unstuck.
If you are tired of feeling productive but having nothing to show for it, this episode is for you.
Want the cheat sheet?
We condensed these 7 shifts into a brand new, high-impact guide: Escape Tutorial Hell. It breaks down every single point we discuss in the episode with actionable steps you can take today.
Listen and Subscribe Here
January 28, 2026
Real Python
How Long Does It Take to Learn Python?
Have you read blog posts that claim you can learn Python in days and quickly secure a high-paying developer job? That’s an unlikely scenario and doesn’t help you prepare for a steady learning marathon. So, how long does it really take to learn Python, and is it worth your time investment?
By the end of this guide, you’ll understand that:
- Most beginners can learn core Python fundamentals in about 2 to 6 months with consistent practice.
- You can write a tiny script in days or weeks, but real confidence comes from projects and feedback.
- Becoming job-ready often takes 6 to 12 months, depending on your background and target role.
- Mastery takes years because the ecosystem and specializations keep growing.
The short answer for how long it takes to learn Python depends on your goals, time budget, and the level you’re aiming for.
Get the PDF Guide: Click here to download a free PDF guide that breaks down how long it takes to learn Python and what factors affect your timeline.
Take the Quiz: Test your knowledge with our interactive “Python Skill Test” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Python Skill TestTest your Python knowledge in a skills quiz with basic to advanced questions. Are you a Novice, Intermediate, Proficient, or Expert?
How Long Does It Take to Learn Python Basics?
Python is beginner-friendly, and you can start writing simple programs in just a few days. But reaching the basics stage still takes consistent practice because you’re learning both the language itself and how to think like a programmer.
The following timeline shows how long it typically takes to learn Python basics based on how much time you can practice each week:
| Weekly practice time | Typical timeline for basics | What that feels like |
|---|---|---|
| 2–3 hours/week | 8–12 months | Slow but steady progress |
| 5–10 hours/week | 3–6 months | Realistic pace for busy adults |
| 15–20 hours/week | ~2 months | Consistent focus and fast feedback |
| 40+ hours/week | ~1 month | Full-time immersion |
These ranges assume about five study days per week. If you add a sixth day, you’ll likely land toward the faster end of each range.
You’ll get better results if you use this table as a planning guide. Don’t think of it as rigid deadlines—your learning pace depends on many factors. For example, if you already know another programming language, then you can usually move faster. If you’re brand-new to coding, then expect to be at the slower end of each range.
As a general guideline, many beginners reach the basics in about 2 to 6 months with steady practice.
Note: If you’re ready to fast-track your learning with an expert-guided small cohort course that gives you live guidance and accountability, then check out Real Python’s live courses!
With a focused schedule of around four hours per day, five days per week, you can often reach the basics stage in roughly 6 to 10 weeks, assuming you’re writing and debugging code most sessions. By then, you should be able to finish several small projects on your own.
When you read online that someone learned Python quickly, they’re probably talking about this basics stage. And indeed, with the right mix of dedication, circumstances, and practice, learning Python basics can happen pretty fast!
Before you go ahead and lock in a timeline, take a moment to clarify for yourself why you want to learn Python. Understanding your motivation for learning Python will help along the way.
Learning Python means more than just learning the Python programming language. You need to know more than just the specifics of a single programming language to do something useful with your programming skills. At the same time, you don’t need to understand every single aspect of Python to be productive.
Learning Python is about learning how to accomplish practical tasks with Python programming. It’s about having a skill set that you can use to build projects for yourself or an employer.
As your next step, write down your personal goal for learning Python. Always keep that goal in mind throughout your learning journey. Your goal shapes what you need to learn and how quickly you’ll progress.
What’s a Practical 30-Day Learning Plan for Complete Beginners?
When you’re clear about your why, you can start drafting your personal Python learning roadmap.
If you’re starting from zero and can spend about 5 to 10 hours per week, the following plan keeps you moving without becoming overwhelming:
- Week 1: Install and set up Python, learn about basic syntax, variables, and conditional statements
- Week 2: Learn about basic data types,
forandwhileloops, and functions - Week 3: Work with lists and dictionaries, file I/O, and debugging basics
- Week 4: Build a small project, add simple tests, and polish it through refactoring
Aim to finish at least one small project by the end of the month. The project matters more than completing every tutorial or task on your checklist.
Read the full article at https://realpython.com/how-long-does-it-take-to-learn-python/ »
[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
PyCharm
PyCharm is designed to support the full range of modern Python workflows, from web development to data and ML/AI work, in a single IDE. An essential part of these workflows is Jupyter notebooks, which are widely used for experimentation, data exploration, and prototyping across many roles.
PyCharm provides first-class support for Jupyter notebooks, both locally and when connecting to external Jupyter servers, with IDE features such as refactoring and navigation available directly in notebooks. Meanwhile, Google Colab has become a key tool for running notebook-based experiments in the cloud, especially when local resources are insufficient.
With PyCharm 2025.3.2, we’re bringing local IDE workflows and Colab-hosted notebooks together. Google Colab support is now available for free in PyCharm as a core feature, along with basic Jupyter notebook support. If you already use Google Colab, you can now bring your notebooks into PyCharm and work with them using IDE features designed for larger projects and longer development sessions.
Getting started with Google Colab in PyCharm
Connecting PyCharm to Colab is quick and straightforward:
- Open a Jupyter notebook in PyCharm.
- Select Google Colab (Beta) from the Jupyter server menu in the top-right corner.
- Sign in to your Google account.
- Create and use a Colab-backed server for the notebook.
Once connected, your notebook behaves as usual, with navigation, inline outputs, tables, and visualizations rendered directly in the editor.
Working with data and files
When your Jupyter notebook depends on files that are not yet available on the Colab machine, PyCharm helps you handle this without interrupting your workflow. If a file is missing, you can upload it directly from your local environment. The remote file structure is also visible in the Project tool window, so you can browse directories and inspect files as you work.
Whether you’re experimenting with data, prototyping models, or working with notebooks that outgrow local resources, this integration makes it easier to move between local work, remote execution, and cloud resources without changing how you work in PyCharm.
If you’d like to try it out:
- Download PyCharm 2025.3.2
- Learn more about Google Colab
EuroPython
January Newsletter: We Want Your Proposals for Kraków!
Happy New Year! We&aposre kicking off 2026 with exciting news: EuroPython is moving to a brand new location! After three wonderful years in Prague, we&aposre heading to Kraków, Poland for our 25th anniversary edition. Mark your calendars for July 13-19, 2026. 🎉
🏰 Welcome to Kraków!
EuroPython 2026 will take place at the ICE Kraków Congress Centre, bringing together 1,500+ Python enthusiasts for a week of learning, networking, and collaboration.
Check out all the details: ep2026.europython.eu/krakow
📣 Call for Proposals is OPEN!
The CfP is now live, and we want to hear from YOU! Whether you&aposre a seasoned speaker or considering your first talk, tutorial or poster, we&aposre looking for proposals on all topics and experience levels.
Deadline: February 15th, 2026 at 23:55 UTC+1 (no extension, so don’t leave it for the last minute!)
We&aposre seeking:
- Talks (30 or 45 min) on any Python-related topic
- Tutorials (hands-on 180 min sessions)
- Posters for the poster session
No matter your level of Python or public speaking experience, EuroPython is here to help you bring yourself to our community. Represent your work, your interests, and your unique perspective!
Want to get some extra help? The first 100 proposals will get direct feedback from the Programme team, so hurry with your submissions!
👉 Submit your proposal by February 15th: programme.europython.eu
🎤 Speaker Mentorship is Open
First time speaking? Feeling nervous? The Speaker Mentorship Programme is back! We match mentees with experienced speakers who&aposll help you craft strong proposals and, if accepted, prepare your talk. This programme especially welcomes folks from underrepresented backgrounds in tech.
Applications are open now for Mentees and Mentors. Don&apost let uncertainty hold you back – apply and join our supportive community of speakers.
Deadline: 10th February 2026, 23:59 UTC
👉 More info: ep2026.europython.eu/mentorship
🎙️ Conversations with First-Time Speakers
Want to hear from people who&aposve been in your shoes? Check out our interviews with first-time speakers who took the leap. They share their experience of what it&aposs really like to speak at EuroPython.
👉 With Jenny Vega: https://youtu.be/0lLrQkPtOy8
👉 With Kayode Oladapo: https://youtu.be/qy7BZUJCYD4
🎥 Video Recap from Prague
Prague was incredible! ✨ Relive the best moments from EuroPython 2025 in our video recap.
📢 Help Us Spread the Word!
Big thanks to our speaker and community organiser Honza Král for giving a lightning talk about EuroPython at Prague Pyvo. If you&aposre a speaker or community organizer, we&aposd love your help spreading the word about the CfP!
💰 Sponsorship & Financial Aid
Sponsorship packages will be announced soon! Interested in supporting EuroPython 2026? Reach out to us at sponsoring@europython.eu.
Financial Aid applications will open in the coming weeks. We&aposre committed to making EuroPython accessible to everyone, regardless of financial situation. Stay tuned!
🤝 Where can you meet us this month?
We&aposll be at FOSDEM this weekend (February 1-2) with a booth alongside the Python Software Foundation and Django Software Foundation. If you&aposre in Brussels, come say hi, grab some stickers, and get the latest EuroPython news!
We&aposre also heading to Ostrava Python Pizza! Join us for tasty pizza and good conversation about all things Python on 21st February.
👋 Stay Connected
Follow us on social media and subscribe to our newsletter for all the updates:
- LinkedIn: https://www.linkedin.com/company/europython/
- X/Twitter: https://x.com/europython
- Mastodon: https://fosstodon.org/@europython
- Bluesky: https://bsky.app/profile/europython.eu
- YouTube: https://www.youtube.com/@EuroPythonConference
- Instagram: https://www.instagram.com/europython/
Hugo van Kemenade
Speeding up Pillow's open and save
Tachyon #
I tried out Tachyon, the new “high-frequency statistical sampling profiler” coming in Python 3.15, to see if we can speed up the Pillow imaging library. I started with a simple script to open an image:
import sys
from PIL import Image
im = Image.open(f"Tests/images/hopper.{sys.argv[1]}")
Then ran:
$ python3.15 -m profiling.sampling run --flamegraph /tmp/1.py png
Captured 35 samples in 0.04 seconds
Sample rate: 1,000.00 samples/sec
Error rate: 25.71
Flamegraph data: 1 root functions, total samples: 26, 169 unique strings
Flamegraph saved to: flamegraph_97927.html
Which generates this flame graph:
The whole thing took 40 milliseconds, with half in Image.py’s open(). If you visit
the interactive HTML page we can see open() calls
preinit(), which in turn imports GifImagePlugin, BmpImagePlugin, PngImagePlugin
and JpegImagePlugin (hover over the <module> boxes to see them).
Do we really need to import all those plugins when we’re only interested in PNG?
Okay, let’s try another kind of image:
$ python3.15 -m profiling.sampling run --flamegraph /tmp/1.py webp
Captured 59 samples in 0.06 seconds
Sample rate: 1,000.00 samples/sec
Error rate: 22.03
Flamegraph data: 1 root functions, total samples: 46, 256 unique strings
Flamegraph saved to: flamegraph_98028.html
Hmm, 60 milliseconds with 80% in open() and most of that in init(). The
HTML page shows it imports AvifImagePlugin, PdfImagePlugin,
WebpImagePlugin, DcxImagePlugin, DdsImagePlugin and PalmImagePlugin. We also
have preinit importing GifImagePlugin, BmpImagePlugin and PngImagePlugin.
Again, why import even more plugins when we only care about WebP?
Loading all the plugins? #
That’s enough profiling, let’s look at the code.
When
open()ing
or
save()ing
an image, if Pillow isn’t yet initialised, we call a
preinit()
function. This loads five drivers for five formats by importing their plugins: BMP, GIF,
JPEG, PPM and PNG.
During import, each plugin registers its file extensions, MIME types and some methods used for opening and saving.
Then we check each of these plugins in turn to see if one will accept the image. Most of Pillow’s plugins detect an image by opening the file and checking if the first few bytes match a magic prefix. For example:
- GIF
starts with
b"GIF87a"orb"GIF89a". - PNG
starts with
b"\211PNG\r\n\032\n"(reference). - JPEG
starts with
b"\xff\xd8\xff", where\xff\xd8means “Start of Image”, and\xffis the start of the next marker (reference).
If none of these five match, we call
init(),
which imports the remaining 42 plugins. We then check each of these for a match.
This has been the case since at least PIL 1.1.1 released in 2000 (this is the oldest version I have to check). There were 33 builtin plugins then and 47 now.
Lazy loading #
This is all a bit wasteful if we only need one or two image formats during a program’s lifetime, especially for things like CLIs. Longer running programs may need a few more, but unlikely all 47.
A benefit of the plugin system is third parties can create their own plugins, but we can be more efficient with our builtins.
I opened a PR to add a mapping of
file extensions to plugins. Before calling preinit() or init(), we can instead do a
cheap lookup, which may save us importing, registering, and checking all those plugins.
Of course, we may have an image without an extension, or with the “wrong” extension, but
that’s fine; I expect it’s rare and anyway we’ll fall back to the original preinit()
-> init() flow.
After merging the PR, here’s a new flame graph for opening PNG (HTML page):
And for WebP (HTML page):
The flame graphs are scaled to the same width, but there’s far fewer boxes meaning there’s much less work now. We’re down from 40 and 60 milliseconds to 20 and 20 milliseconds.
The PR has a bunch of benchmarks which show opening a PNG (that previously loaded five plugins) is now 2.6 times faster. Opening a WebP (that previously loaded all 47 plugins), is now 14 times faster. Similarly, Saving PNG is improved by 2.2 times and WebP by 7.9 times. Success! This will be in Pillow 12.2.0.
See also #
-
Henry Schreiner on making packaging faster.
-
Adam Johnson’s tprof is another new tool which is useful for things like this.
EuroPython
Humans of EuroPython: Rodrigo Girão Serrão
EuroPython depends entirely on the dedication of volunteers who invest tremendous effort into bringing it to life. From managing sponsor relationships and designing the event schedule to handling registration systems and organizing social events, countless hours of passionate work go into ensuring each year surpasses the last.
Discover our recent conversation with Rodrigo Girão Serrão, who served on the EuroPython 2025 Programme Team.
We&aposre grateful for your work on the conference programme, Rodrigo!
EP: Had you attended EuroPython before volunteering, or was volunteering your first experience with it?
When I attended my first EuroPython in person I was not officially a volunteer but ended up helping a bit. Over the years, my involvement with EuroPython as a volunteer and organiser has been increasing exponentially!
EP: Are there any new skills you learned while volunteering at EuroPython? If so, which ones?
Volunteering definitely pushed me to develop many skills. As an example, hosting the sprints developed my social skills since I had to welcome all the participants and ensure they had everything they needed. It also improved my management skills, from supporting the project sprint organisers to coordinating with venue staff.
EP: Did you have any unexpected or funny experiences during EuroPython?
In a recent EuroPython someone came up to me after my tutorial and said something like “I doubted your tutorial was going to be good, but in the end it was good”. Why on Earth would that person doubt me in the first place and then come to me and admit it? 🤣
EP: Did you make any lasting friendships or professional connections through volunteering?
Yes to both! Many of these relationships grew over time through repeated interactions across multiple EuroPython editions and also other conferences. Volunteering created a sense of continuity and made it much easier to connect with the same people year after year.
EP: If you were to invite someone else, what do you think are the top 3 reasons to join the EuroPython organizing team?
Nothing beats the smiles and thank you’s you get when the conference is over. Plus, it is an amazing feeling to be part of something bigger than yourself.
EP: Would you volunteer again, and why?
Hell yeah! See above :)
EP: Thanks, Rodrigo!
PyCharm
Google Colab Support Is Now Available in PyCharm 2025.3.2
Python Morsels
All iteration is the same in Python
In Python, for loops, list comprehensions, tuple unpacking, and * unpacking all use the same iteration mechanism.
Looping over dictionaries gives keys
When you loop over a dictionary, you'll get the keys in that dictionary:
>>> my_dict = {'red': 2, 'blue': 3, 'green': 4}
>>> for thing in my_dict:
... print(thing)
...
red
blue
green
If you loop over a dictionary in a list comprehensions, you'll also get keys:
>>> names = [x.upper() for x in my_dict]
>>> names
['RED', 'BLUE', 'GREEN']
Iterable unpacking with * also relies on iteration.
So if we use this to iterate over a dictionary, we again get the keys:
>>> print(*my_dict)
red blue green
The same thing happens if we use * to unpack a dictionary into a list:
>>> colors = ["purple", *my_dict]
>>> colors
['purple', 'red', 'blue', 'green']
And even tuple unpacking relies on iteration. Anything you can loop over can be unpacked. Since we know there are three items in our dictionary, we could unpack it:
>>> a, b, c = my_dict
And of course, as strange as it may seem, we get the keys in our dictionary when we unpack it:
>>> a
'red'
>>> b
'blue'
So what would happen if we turned our dictionary into a list by passing it to the list constructor?
>>> list(my_dict)
Well, list will loop over whatever iterable was given to it and make a new list out of it.
And when we loop over a dictionary, what do we get?
The keys:
>>> list(my_dict)
['red', 'blue', 'green']
And of course, if we ask whether something is in a dictionary, we are asking about the keys:
>>> 'blue' in my_dict
True
Iterating over a dictionary object in Python will give you keys, no matter what Python feature you're using to do that iteration. All forms of iteration do the same thing in Python.
Aside: of course if you want key-value pairs you can get them using the dictionary items method.
Looping over strings provides characters
Strings are also iterables.
Read the full article: https://www.pythonmorsels.com/all-iteration-is-the-same/
January 27, 2026
Giampaolo Rodola
From Python 3.3 to today: ending 15 years of subprocess polling
One of the less fun aspects of process management on POSIX systems is waiting
for a process to terminate. The standard library's subprocess module has
relied on a busy-loop polling approach since the timeout parameter was added
to
Popen.wait()
in Python 3.3, around 15 years ago (see
source).
And psutil's
Process.wait()
method uses exactly the same technique (see
source).
The logic is straightforward: check whether the process has exited using
non-blocking waitpid(WNOHANG), sleep briefly, check again, sleep a bit
longer, and so on.
import os, time
def wait_busy(pid, timeout):
end = time.monotonic() + timeout
interval = 0.0001
while time.monotonic() < end:
pid_done, _ = os.waitpid(pid, os.WNOHANG)
if pid_done:
return
time.sleep(interval)
interval = min(interval * 2, 0.04)
raise TimeoutExpired
In this blog post I'll show how I finally addressed this long-standing inefficiency, first in psutil, and most excitingly, directly in CPython's standard library subprocess module.
The problem with busy-polling
- CPU wake-ups: even with exponential backoff (starting at 0.1ms, capping at 40ms), the system constantly wakes up to check process status, wasting CPU cycles and draining batteries.
- Latency: there's always a gap between when a process actually terminates and when you detect it.
- Scalability: monitoring many processes simultaneously magnifies all of the above.
Event-driven waiting
All POSIX systems provide at least one mechanism to be notified when a file descriptor becomes ready. These are select(), poll(), epoll() (Linux) and kqueue() (BSD / macOS) system calls. Until recently, I believed they could only be used with file descriptors referencing sockets, pipes, etc., but it turns out they can also be used to wait for events on process PIDs!
Linux
In 2019, Linux 5.3 introduced a new syscall,
pidfd_open(),
which was added to the os module in Python 3.9. It returns a file descriptor
referencing a process PID. The interesting thing is that pidfd_open() can be
used in conjunction with select(), poll() or epoll() to effectively wait
until the process exits. E.g. by using poll():
import os, select
def wait_pidfd(pid, timeout):
pidfd = os.pidfd_open(pid)
poller = select.poll()
poller.register(pidfd, select.POLLIN)
# block until process exits or timeout occurs
events = poller.poll(timeout * 1000)
if events:
return
raise TimeoutError
This approach has zero busy-looping. The kernel wakes us up exactly when the process terminates or when the timeout expires if the PID is still alive.
I chose poll() over select() because select() has a historical file
descriptor limit (FD_SETSIZE), which typically caps it at 1024 file
descriptors per-process (reminded me of
BPO-1685000).
I chose poll() over epoll() because it does not require creating an
additional file descriptor. It also needs only a single syscall, which should
make it a bit more efficient when monitoring a single FD rather than many.
macOS and BSD
BSD-derived systems (including macOS) provide the kqueue() syscall. It's
conceptually similar to select(), poll() and epoll(), but more powerful
(e.g. it can also handle regular files). kqueue() can be passed a PID
directly, and it will return once the PID disappears or the timeout expires:
import select
def wait_kqueue(pid, timeout):
kq = select.kqueue()
kev = select.kevent(
pid,
filter=select.KQ_FILTER_PROC,
flags=select.KQ_EV_ADD | select.KQ_EV_ONESHOT,
fflags=select.KQ_NOTE_EXIT,
)
# block until process exits or timeout occurs
events = kq.control([kev], 1, timeout)
if events:
return
raise TimeoutError
Windows
Windows does not busy-loop, both in psutil and subprocess module, thanks to
WaitForSingleObject. This means Windows has effectively had event-driven
process waiting from the start. So nothing to do on that front.
Graceful fallbacks
Both pidfd_open() and kqueue() can fail for different reasons. For example,
with EMFILE if the process runs out of file descriptors (usually 1024), or
with EACCES / EPERM if the syscall was explicitly blocked at the system
level by the sysadmin (e.g. via SECCOMP). In all cases, psutil silently falls
back to the traditional busy-loop polling approach rather than raising an
exception.
This fast-path-with-fallback approach is similar in spirit to
BPO-33671, where I sped up
shutil.copyfile() by using zero-copy system calls back in 2018. In there,
more efficient os.sendfile() is attempted first, and if it fails (e.g. on
network filesystems) we fall back to the traditional read() / write()
approach to copy regular files.
Measurement
As a simple experiment, here's a simple program which waits on itself for 10 seconds without terminating:
# test.py
import psutil, os
try:
psutil.Process(os.getpid()).wait(timeout=10)
except psutil.TimeoutExpired:
pass
We can measure the CPU context switching using /usr/bin/time -v. Before the
patch (the busy-loop):
$ /usr/bin/time -v python3 test.py 2>&1 | grep context
Voluntary context switches: 258
Involuntary context switches: 4
After the patch (the event-driven approach):
$ /usr/bin/time -v python3 test.py 2>&1 | grep context
Voluntary context switches: 2
Involuntary context switches: 1
This shows that instead of spinning in userspace, the process blocks in
poll() / kqueue(), and is woken up only when the kernel notifies it,
resulting in just a few CPU context switches.
Sleeping state
It's also interesting to note that waiting via poll() (or kqueue()) puts
the process into the exact same sleeping state as a plain time.sleep() call.
From the kernel's perspective, both are interruptible sleeps: the process is
de-scheduled, consumes zero CPU, and sits quietly in kernel space.
The "S+" state shown below by ps means that the process "sleeps in
foreground".
time.sleep():
$ (python3 -c 'import time; time.sleep(10)' & pid=$!; sleep 0.3; ps -o pid,stat,comm -p $pid) && fg &>/dev/null
PID STAT COMMAND
491573 S+ python3
poll():
$ (python3 -c 'import os,select; fd = os.pidfd_open(os.getpid(),0); p = select.poll(); p.register(fd,select.POLLIN); p.poll(10_000)' & pid=$!; sleep 0.3; ps -o pid,stat,comm -p $pid) && fg &>/dev/null
PID STAT COMMAND
491748 S+ python3
CPython contribution
After landing the psutil implementation
(psutil/PR-2706), I took the
extra step and submitted a matching pull request for CPython subprocess
module: cpython/PR-144047.
I'm especially proud of this one: this is the second time in psutil's 17+
year history that a feature developed in psutil made its way upstream into the
Python standard library. The first was back in 2011, when psutil.disk_usage()
inspired
shutil.disk_usage() (see
python-ideas ML proposal).
Funny thing: 15 years ago, Python 3.3 added the timeout parameter to
subprocess.Popen.wait() (see
commit). That's
probably where I took inspiration when I first added the timeout parameter to
psutil's Process.wait() around the same time (see
commit). Now, 15 years
later, I'm contributing back a similar improvement for that very same timeout
parameter. The circle is complete.
Links
Topics related to this:
- psutil/#2712: proposal to
extend this to multiple PIDs (
psutil.wait_procs()). - psutil/#2703: proposal for
asynchronous
psutil.Process.wait()integration withasyncio. - cpython/#144211: proposal
to extend the selectors
module to enable
asynciooptimization on BSD / macOS viakqueue().
Discussion
PyCoder’s Weekly
Issue #719: Django Tasks, Dictionaries, Ollama, and More (Jan. 27, 2026)
#719 – JANUARY 27, 2026
View in Browser »
Migrating From Celery to Django Tasks
Django 6 introduced the new tasks framework, a general interface for asynchronous tasks. This article shows you how to go from Celery specific code to the new general purpose mechanism.
PAUL TRAYLOR
The Hidden Cost of Python Dictionaries
Learn why Python dicts cause silent bugs and how NamedTuple, dataclass, and Pydantic catch errors earlier with better error messages.
CODECUT.AI • Shared by Khuyen Tran
Python Errors? Fix ‘em Fast for FREE with Honeybadger
If you support web apps in production, you need intelligent logging with error alerts and de-duping. Honeybadger filters out the noise and transforms Python logs into contextual issues so you can find and fix errors fast. Get your FREE account →
HONEYBADGER sponsor
How to Integrate Local LLMs With Ollama and Python
Learn how to integrate your Python projects with local models (LLMs) using Ollama for enhanced privacy and cost efficiency.
REAL PYTHON
Articles & Tutorials
Nothing to Declare: From NaN to None via null
Explore the key differences between NaN, null, and None in numerical data handling using Python. While all signal “no meaningful value,” they behave differently. Learn about the difference and how to correctly handle the data using Pydantic models and JSON serialization.
FMULARCZYK.PL • Shared by Filip Mularczyk
Continuing to Improve the Learning Experience at Real Python
If you haven’t visited the Real Python website lately, then it’s time to check out a great batch of updates on realpython.com! Dan Bader returns to the show this week to discuss improvements to the site and more ways to learn Python.
REAL PYTHON podcast
The Ultimate Guide to Docker Build Cache
Docker builds feel slow because cache invalidation is working against you. Depot explains how BuildKit’s layer caching works, when to use bind mounts vs cache mounts, and how to optimize your Dockerfile so Gradle dependencies don’t rebuild on every code change →
DEPOT sponsor
The State of WebAssembly: 2025 and 2026
A comprehensive look at WebAssembly in 2025 and 2026, covering browser support, Safari updates, WebAssembly 3.0, WASI, .NET, Kotlin, debugging improvements, and growing adoption across edge computing and embedded devices.
GERARD GALLANT
Asyncio Is Neither Fast Nor Slow
There are many misconceptions on asyncio, as such there are many misleading benchmarks out there. This article looks at how to analyse a benchmark result and to come up with more relevant conclusions.
CHANGS.CO.UK • Shared by Jamie Chang
Expertise Is the Art of Ignoring
Kevin says that trying to “master” a programming language is a trap. Real expertise comes from learning what you need, when you need it, and ignoring the rest on purpose.
KEVIN RENSKERS
uv vs pip: Python Packaging and Dependency Management
Choosing between uv vs pip? This video course compares speed, reproducible environments, compatibility, and dependency management to help you pick the right tool.
REAL PYTHON course
Ee Durbin Departing the Python Software Foundation
Ee Durbin is a long time contributor to Python and was heavily involved in the community even before becoming a staff member at the PSF. Ee is moving on though.
PYTHON SOFTWARE FOUNDATION
Self-Concatenation
Strings and other sequences can be multiplied by numbers to self-concatenate them. You need to be careful with mutable sequences though.
TREY HUNNER
Python, Is It Being Killed by Incremental Improvements?
This opinion piece asks whether Python’s recent focus on concurrency is a misstep and whether efforts should be focused elsewhere.
STEFAN-MARR.DE
How to Parametrize Exception Testing in PyTest?
A quick TIL-style article on how to provide different input data and test different exceptions being raised in pytest.
BORUTZKI
Projects & Code
django-nis2-shield: NIS2 Compliance Middleware
GITHUB.COM/NIS2SHIELD • Shared by Fabrizio Di Priamo
pfst: AST Manipulation That Preserves Formatting
GITHUB.COM/TOM-PYTEL • Shared by Tomasz Pytel
Events
Weekly Real Python Office Hours Q&A (Virtual)
January 28, 2026
REALPYTHON.COM
Python Devroom @ FOSDEM 2026
January 31 to February 1, 2026
FOSDEM.ORG
Melbourne Python Users Group, Australia
February 2, 2026
J.MP
PyBodensee Monthly Meetup
February 2, 2026
PYBODENSEE.COM
STL Python
February 5, 2026
MEETUP.COM
Happy Pythoning!
This was PyCoder’s Weekly Issue #719.
View in Browser »
[ Subscribe to 🐍 PyCoder’s Weekly 💌 – Get the best Python news, articles, and tutorials delivered to your inbox once a week >> Click here to learn more ]
death and gravity
DynamoDB crash course: part 1 – philosophy
This is part one of a series covering core DynamoDB concepts and patterns, from the data model and features all the way up to single-table design.
The goal is to get you to understand what idiomatic usage looks like and what the trade-offs are in under an hour, providing entry points to detailed documentation.
(Don't get me wrong, the AWS documentation is comprehensive, but can be quite complex, and DynamoDB being a relatively low level product with lots of features added over the years doesn't really help with that.)
Today, we're looking at what DynamoDB is and why it is the way it is.
What is DynamoDB? #
Quoting Wikipedia:
Amazon DynamoDB is a managed NoSQL database service provided by AWS. It supports key-value and document data structures and is designed to handle a wide range of applications requiring scalability and performance.
See also
This definition should suffice for now; for a more detailed refresher, see:
The DynamoDB data model can be summarized as follows:
A table is a collection of items, and an item is a collection of attributes. Items are uniquely identified by two attributes, the partition key and the sort key. The partition key determines where (i.e. on what computer) an item is stored. The sort key is used to get ordered ranges of items from a specific partition.
That's is, that's the whole data model. Sure, there's indexes and transactions and other features, but at its core, this is it. Put another way:
A DynamoDB table is a hash table of B-trees1 – partition keys are hash table keys, and sort keys are B-tree keys. Because of this, any access not based on partition and sort key is expensive, since you end up doing a full table scan.
If you were to implement this model in Python, it'd look something like this:
from collections import defaultdict
from sortedcontainers import SortedDict
class Table:
def __init__(self, pk_name, sk_name):
self._pk_name = pk_name
self._sk_name = sk_name
self._partitions = defaultdict(SortedDict)
def put_item(self, item):
pk, sk = item[self._pk_name], item[self._sk_name]
old_item = self._partitions[pk].setdefault(sk, {})
old_item.clear()
old_item.update(item)
def get_item(self, pk, sk):
return dict(self._partitions[pk][sk])
def query(self, pk, minimum=None, maximum=None, inclusive=(True, True), reverse=False):
# in the real DynamoDB, this operation is paginated
partition = self._partitions[pk]
for sk in partition.irange(minimum, maximum, inclusive, reverse):
yield dict(partition[sk])
def scan(self):
# in the real DynamoDB, this operation is paginated
for partition in self._partitions.values():
for item in partition.values():
yield dict(item)
def update_item(self, item):
pk, sk = item[self._pk_name], item[self._sk_name]
old_item = self._partitions[pk].setdefault(sk, {})
old_item.update(item)
def delete_item(self, pk, sk):
del self._partitions[pk][sk]
>>> table = Table('Artist', 'Song')
>>>
>>> table.put_item({'Artist': '1000mods', 'Song': 'Vidage', 'Year': 2011})
>>> table.put_item({'Artist': '1000mods', 'Song': 'Claws', 'Album': 'Vultures'})
>>> table.put_item({'Artist': 'Kyuss', 'Song': 'Space Cadet'})
>>>
>>> table.get_item('1000mods', 'Claws')
{'Artist': '1000mods', 'Song': 'Claws', 'Album': 'Vultures'}
>>> [i['Song'] for i in table.query('1000mods')]
['Claws', 'Vidage']
>>> [i['Song'] for i in table.query('1000mods', minimum='Loose')]
['Vidage']
Philosophy #
One can't help but feel this kind of simplicity would be severely limiting.
A consequence of DynamoDB being this low level is that, unlike with most relational databases, query planning and sometimes index management happen at the application level, i.e. you have to do them yourself in code. In turn, this means you need to have a clear, upfront understanding of your application's access patterns, and accept that changes in access patterns will require changes to the application.
In return, you get a fully managed, highly-available database that scales infinitely:2 there are no servers to take care of, there's almost no downtime, and there are no limits on table size or the number of items in a table; where limits do exist, they are clearly documented, allowing for predictable performance.
This highlights an intentional design decision that is essentially DynamoDB's main proposition to you as its user: data modeling complexity is always preferable to complexity coming from infrastructure maintenance, availability, and scalability (what AWS marketing calls "undifferentiated heavy lifting").
To help manage this complexity, a number of design patterns have arisen, covered extensively by the official documentation, and which we'll discuss in a future article. Even so, the toll can be heavy – by AWS's own admission, the prime disadvantage of single table design, the fundamental design pattern, is that:
[the] learning curve can be steep due to paradoxical design compared to relational databases
As this walkthrough puts it:
a well-optimized single-table DynamoDB layout looks more like machine code than a simple spreadsheet
...which, admittedly, sounds pretty cool, but also why would I want that? After all, most useful programming most people do is one or two abstraction levels above assembly, itself one over machine code.
See also
- NoSQL design
- (unofficial) # The DynamoDB philosophy of limits
A bit of history #
Perhaps it's worth having a look at where DynamoDB comes from.
Amazon.com used Oracle databases for a long time. To cope with the increasing scale, they first adopted a database-per-service model, and then sharding, with all the architectural and operational overhead you would expect. At its 2017 peak (five years after DynamoDB was released in AWS, and over ten years after some version of it was available internally), they still had 75 PB of data in nearly 7500 Oracle databases, owned by 100+ teams, with thousands of applications, for OLTP workloads alone. That sounds pretty traumatic – it was definitely bad enough to allegedly ban OLTP relational databases internally, and require that teams get VP approval to use one.
Yeah, coming from that, it's hard to argue DynamoDB adds complexity.
That is not to say relational databases cannot be as scalable as DynamoDB, just that Amazon doesn't belive in them – distributed SQL databases like Google's Spanner and CockroachDB have existed for a while now, and even AWS seems to be warming up to the idea.
This might also explain why the design patterns are so slow to make their way into SDKs, or even better, into DynamoDB itself; when you have so many applications and so many experienced teams, the cost of yet another bit of code to do partition key sharding just isn't that great.
See also
- (paper) Amazon DynamoDB: A Scalable, Predictably Performant, and Fully Managed NoSQL Database Service (2022)
- (paper) Dynamo: Amazon’s Highly Available Key-value Store (2007)
Anyway, that's it for now.
In the next article, we'll have a closer look at the DynamoDB data model and features.
Learned something new today? Share it with others, it really helps!
Want to know when new articles come out? Subscribe here to get new stuff straight to your inbox!
Or any other sorted data structure that allows fast searches, sequential access, insertions, and deletions. [return]
As the saying goes, the cloud is just someone else's computers. Here, "infinitely" means it scales horizontally, and you'll run out of money before AWS runs out of computers. [return]
Real Python
Create Callable Instances With Python's .__call__()
In Python, a callable is any object that you can call using a pair of parentheses and, optionally, a series of arguments. Functions, classes, and methods are all common examples of callables in Python. Besides these, you can also create custom classes that produce callable instances. To do this, you can add the .__call__() special method to your class.
Instances of a class with a .__call__() method behave like functions, providing a flexible and handy way to add functionality to your objects. Understanding how to create and use callable instances is a valuable skill for any Python developer.
In this video course, you’ll:
- Understand the concept of callable objects in Python
- Create callable instances by adding a
.__call__()method to your classes - Compare
.__init__()and.__call__()and understand their distinct roles - Build practical examples that use callable instances to solve real-world problems
[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]
PyBites
The missing 66% of your skillset
Bob and I have spent many years as Python devs, and 6 years coaching with Pybites and we can safely say that being a Senior Developer is only about 1/3 Python knowledge.
The other 60% is the ecosystem. It’s the tooling. It’s all of the tech around Python that makes you stand out from the rest.
This is the biggest blind spot keeping developers stuck in Tutorial Hell. You spend hours memorising obscure library features, but you crumble when asked to configure a CI/CD pipeline. (That’s not just made up by the way – many of you in dev roles will have seen this with colleagues at some point or another!)
These are the elements of the Python ecosystem you should absolutely be building experience with if you want to move from being a scripter to an engineer:
- Dependency Management: Stop using pip freeze. Look at uv.
- Git: Not just add/commit. Learn branching strategies and how to fix a merge conflict without panicking.
- Testing: print() is not a test. Learn pytest and how to write good tests.
- Quality Control: Set up Linters (Ruff) so you stop arguing about formatting, and ty for type checking.
- Automation: Learn GitHub Actions (CI/CD). Make the robots run your tests for you.
- Deployment: How does your code get to a server? Learn basic Docker and Cloud.
- The CLI: Stop clicking buttons and get comfortable in the terminal. Learn Makefiles and create a make install or make test command to save your sanity.
It looks like a lot. It is a lot. But this is the difference between a hobbyist and a professional.
Does this make you feel overwhelmed? Or does it give you a roadmap of what to do this year?
I’m curious! Feel free to hit me up in the Community with your thoughts.
And yes, these are all things we coach people on in PDM. Use the link below to have a chat.
Julian
This note was originally sent to our email list. Join here: https://pybit.es/newsletter
HoloViz
A Major Step Toward Structured, Auditable AI-Driven Data Apps: Lumen AI 1.0
Seth Michael Larson
Use “\A...\z”, not “^...$” with Python regular expressions
Two years ago I discovered a potential foot-gun
with the Python standard library “re” module.
I blogged about this behavior,
and turns out that
I wasn't only one who didn't know this:
The article was #1 on HackerNews and the
most-read article on my blog in 2024.
In short the unexpected behavior is that the pattern “^Hello$” matches both “Hello” and “Hello\n”,
and sometimes you don't intend to match a trailing newline.
This article serves as a follow-up!
Back in 2024
I created a table showing that \z was a partially viable
alternative to $ for matching end-of-string
without matching a trailing newline... for every regular expression
implementation EXCEPT Python and EMCAScript.
But that is no longer true, Python 3.14 now supports \z! This means \z is one step closer
to being the universal recommendation to match
the end of string without matching a newline.
Obviously no one is upgrading their Python
version just for this new feature, but it's good to know that
the gap is being closed. Thanks to David Wheeler
for doing deeper research in the OpenSSF Best Practices
WG and publishing this report.
Until Python 3.13 is deprecated and long gone: using \Z (as an alias for \z) works fine for Python regular expressions.
Just note that this behavior isn't the same across regular expression
implementations, for example EMCAScript, Golang, and Rust
don't support \Z and for PHP, Java, and .NET \Z actually
matches trailing newlines!
Thanks for keeping RSS alive! ♥
Armin Ronacher
Colin and Earendil
Regular readers of this blog will know that I started a new company. We have put out just a tiny bit of information today, and some keen folks have discovered and reached out by email with many thoughtful responses. It has been delightful.
Colin and I met here, in Vienna. We started sharing coffees, ideas, and lunches, and soon found shared values despite coming from different backgrounds and different parts of the world. We are excited about the future, but we’re equally vigilant of it. After traveling together a bit, we decided to plunge into the cold water and start a company together. We want to be successful, but we want to do it the right way and we want to be able to demonstrate that to our kids.
Vienna is a city of great history, two million inhabitants and a fascinating vibe that is nothing like San Francisco. In fact, Vienna is in many ways the polar opposite to the Silicon Valley, both in mindset, in opportunity and approach to life. Colin comes from San Francisco, and though I’m Austrian, my career has been shaped by years working with California companies and people from there who used my Open Source software. Vienna is now our shared home. Despite Austria being so far away from California, it is a place of tinkerers and troublemakers. It’s always good to remind oneself that society consists of more than just your little bubble. It also creates the necessary counter balance to think in these times.
The world that is emerging in front of our eyes is one of change. We incorporated as a PBC with a founding charter to craft software and open protocols, strengthen human agency, bridge division and ignorance and to cultivate lasting joy and understanding. Things we believe in deeply.
I have dedicated 20 years of my life in one way or another creating Open Source software. In the same way as artificial intelligence calls into question the very nature of my profession and the way we build software, the present day circumstances are testing society. We’re not immune to these changes and we’re navigating them like everyone else, with a mixture of excitement and worry. But we share a belief that right now is the time to stand true to one’s values and principles. We want to take an earnest shot at leaving the world a better place than we found it. Rather than reject the changes that are happening, we look to nudge them towards the right direction.
If you want to follow along you can subscribe to our newsletter, written by humans not machines.
January 26, 2026
Real Python
GeoPandas Basics: Maps, Projections, and Spatial Joins
GeoPandas extends pandas to make working with geospatial data in Python intuitive and powerful. If you’re looking to do geospatial tasks in Python and want a library with a pandas-like API, then GeoPandas is an excellent choice. This tutorial shows you how to accomplish four common geospatial tasks: reading in data, mapping it, applying a projection, and doing a spatial join.
By the end of this tutorial, you’ll understand that:
- GeoPandas extends pandas with support for spatial data. This data typically lives in a
geometrycolumn and allows spatial operations such as projections and spatial joins, while Folium focuses on richer interactive web maps after data preparation. - You inspect CRS with
.crsand reproject data using.to_crs()with an authority code likeEPSG:4326orESRI:54009. - A geographic CRS stores longitude and latitude in degrees, while a projected CRS uses linear units like meters or feet for area and distance calculations.
- Spatial joins use
.sjoin()with predicates like"within"or"intersects", and both inputs must share the same CRS or the relationships will be computed incorrectly.
Here’s how GeoPandas compares with alternative libraries:
| Use Case | Pick pandas | Pick Folium | Pick GeoPandas |
|---|---|---|---|
| Tabular data analysis | ✅ | - | ✅ |
| Mapping | - | ✅ | ✅ |
| Projections, spatial joins | - | - | ✅ |
GeoPandas builds on pandas by adding support for geospatial data and operations like projections and spatial joins. It also includes tools for creating maps. Folium complements this by focusing on interactive, web-based maps that you can customize more deeply.
Get Your Code: Click here to download the free sample code for learning how to work with GeoPandas maps, projections, and spatial joins.
Take the Quiz: Test your knowledge with our interactive “GeoPandas Basics: Maps, Projections, and Spatial Joins” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
GeoPandas Basics: Maps, Projections, and Spatial JoinsTest GeoPandas basics for reading, mapping, projecting, and spatial joins to handle geospatial data confidently.
Getting Started With GeoPandas
You’ll first prepare your environment and load a small dataset that you’ll use throughout the tutorial. In the next two subsections, you’ll install the necessary packages and read in a sample dataset of New York City borough boundaries. This gives you a concrete GeoDataFrame to explore as you learn the core concepts.
Installing GeoPandas
This tutorial uses two packages: geopandas for working with geographic data and geodatasets for loading sample data. It’s a good idea to install these packages inside a virtual environment so your project stays isolated from the rest of your system and you can manage its dependencies cleanly.
Once your virtual environment is active, you can install both packages with pip:
$ python -m pip install "geopandas[all]" geodatasets
Using the [all] option ensures you have everything needed for reading data, transforming coordinate systems, and creating plots. For most readers, this will work out of the box.
If you do run into installation issues, the project’s maintainers provide alternative installation options on the official installation page.
Reading in Data
Most geospatial datasets come in GeoJSON or shapefile format. The read_file() function can read both, and it accepts either a local file path or a URL.
In the example below, you’ll use read_file() to load the New York City Borough Boundaries (NYBB) dataset. The geodatasets package provides a convenient path to this dataset, so you don’t need to download anything manually. You’ll also drop unnecessary columns:
>>> import geopandas as gpd
>>> import matplotlib.pyplot as plt
>>> from geodatasets import get_path
>>> path_to_data = get_path("nybb")
>>> nybb = gpd.read_file(path_to_data)
>>> nybb = nybb[["BoroName", "Shape_Area", "geometry"]]
>>> nybb
BoroName Shape_Area geometry
0 Staten Island 1.623820e+09 MULTIPOLYGON (((970217.022 145643.332, ....
1 Queens 3.045213e+09 MULTIPOLYGON (((1029606.077 156073.814, ...
2 Brooklyn 1.937479e+09 MULTIPOLYGON (((1021176.479 151374.797, ...
3 Manhattan 6.364715e+08 MULTIPOLYGON (((981219.056 188655.316, ....
4 Bronx 1.186925e+09 MULTIPOLYGON (((1012821.806 229228.265, ...
>>> type(nybb)
<class 'geopandas.geodataframe.GeoDataFrame'>
>>> type(nybb["geometry"])
<class 'geopandas.geoseries.GeoSeries'>
nybb is a GeoDataFrame. A GeoDataFrame has rows, columns, and all the methods of a pandas DataFrame. The difference is that it typically includes a special geometry column, which stores geographic shapes instead of plain numbers or text.
The geometry column is a GeoSeries. It behaves like a normal pandas Series, but its values are spatial objects that you can map and run spatial queries against. In the nybb dataset, each borough’s geometry is a MultiPolygon—a shape made of several polygons—because every borough consists of multiple islands. Soon you’ll use these geometries to make maps and run spatial operations, such as finding which borough a point falls inside.
Mapping Data
Once you’ve loaded a GeoDataFrame, one of the quickest ways to understand your data is to visualize it. In this section, you’ll learn how to create both static and interactive maps. This allows you to inspect shapes, spot patterns, and confirm that your geometries look the way you expect.
Creating Static Maps
Read the full article at https://realpython.com/geopandas/ »
[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]