Planet Python
Last update: August 04, 2025 01:43 PM UTC
August 04, 2025
Real Python
Quiz: Skip Ahead in Loops With Python's Continue Keyword
In this quiz, you’ll test your understanding of
Python’s continue
statement.
The continue
statement allows you to skip code in a loop for the current iteration,
jumping immediately to the next iteration. It’s used exclusively in for
and while
loops,
allowing you to control the flow of execution, bypass specific conditions,
and continue processing in a structured and predictable manner.
[ 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 ]
Quiz: Working With Python's Built-in Exceptions
In this quiz, you’ll revisit Python’s built-in exceptions, exploring their hierarchy and how to handle them gracefully. You’ll practice distinguishing between errors and exceptions, using try...except
blocks, and identifying specific exceptions like IndexError
and GeneratorExit
.
Brush up your skills by reviewing the Working With Python’s Built-in Exceptions course before you start!
[ 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 ]
Quiz: Using the "or" Boolean Operator in Python
In this quiz, you’ll test your understanding of the Python or
Operator.
You’ll learn how to use it in both Boolean and non-Boolean contexts,
and understand how it can be used to solve programming 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 ]
Quiz: Python Namespace Packages
In this quiz, you’ll practice your knowledge about Python’s namespace packages.
What are they used for? How do you set up a namespace package? How could you create one before PEP 420? Complete this quick quiz to test your knowledge.
[ 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 ]
EuroPython Society
EuroPython 2025 Code of Conduct Transparency Report
The 2025 version of the EuroPython conference took place both online and in person in July 2025. This was the third conference under our current Code of Conduct (CoC), and we had Code of Conduct working group members continuously available both online and in person.
Reports
Over the course of the conference, the Code of Conduct team was made aware of the following issue:
- One person was uncomfortable with certain phrases being used in one of the poster sessions. The author was informed, and the phrases reported were removed by the author from their poster presentation promptly.
Thank you the Code of Conduct team responded to the issue reported.
Tryton News
Announcing āCode to Careā: A Hands-On Tryton Technical Workshop (Aug 15ā17)
Code to Care ā A Hands-On Tryton Technical Workshop
We are excited to announce āCode to Careā, a 3-day live hands-on workshop on Tryton ERP customization, with a special focus on GNUHealth.
- Dates: August 15ā17, 2025
- Mode: Online (Live Interactive Sessions)
- Fee: ā¹20,000 per participant
- Language: English
- Register Interest: Expression of Interest ā Code to Care: Tryton x GNUHealth Customisation Workshop
About the Trainer
With over 10 years of hands-on experience with Tryton, the trainer has successfully implemented Tryton at Indiaās largest medical institute, managing key domains such as HR, Payroll, Procurement, and clinical modules.
What Youāll Explore
- How to customise the existing modules of Tryton for your systemās needs
- How to build new modules to support or enhance your workflows and functionalities
- Deployment and maintenance best practices
- Contributing to the Tryton and GNUHealth ecosystems
Giving Back
A portion of the participation fee will be donated to the:
- Tryton Foundation
- GNUHealth Foundation
- openSUSE Project
Further details will be shared with the interested participants over registered email address. Register your interest at Expression of Interest ā Code to Care: Tryton x GNUHealth Customisation Workshop
1 post - 1 participant
August 03, 2025
The Python Coding Stack
Flashy, Fancy Shortcuts Aren't Always Suitable [Python Shorts]
In the previous post, I talked about Python's or
keyword and how it doesn't behave the way you may expect it to. Here's the link to that article if you missed it: Do You Really Know How `or` And `and` Work in Python?
One place you may see the or
expression used is when dealing with the infamous mutable default value problem in functions–see the second section in Python Quirks? Party Tricks? Peculiarities Revealed… if you're not familiar with this Python banana skin. It seems some LLM models are keen to suggest this option, too.
To keep this post brief, I'll assume you're familiar with both how or
works and the mutable default value issue.
Let's see how the or
keyword is used to solve the mutable default value problem:

I'm using this function for demonstration purposes. Note that the function mutates the list and returns it.
But let's look at the part that's more relevant for this post. This function uses an empty list if no value is passed to the shopping_list
parameter. Since you can't use an empty list as the default value, you use None
as the default value.
The or
expression then does all the hard work:
If you don't pass a list to the
shopping_list
parameter when you calladd_to_shopping_list()
, the function uses theNone
default value forshopping_list
. And sinceNone
is falsy, theor
expression evaluates to its second operand, the empty list[]
.However, if you already have a list with items in it and you pass it to the
shopping_list
parameter when you calladd_to_shopping_list()
, then this list is the one used within the function.
Let's try it out to confirm this is how the function works.
First, try with an existing list:
You create a food_groceries
list containing a few items. You then pass it to add_to_shopping_list()
. You display output
and food_groceries
—these are names referring to the same list. They're not different lists. You can refresh your memory about Python's pass-by-assignment in functions here: If You Haven't Got A Clue What "Pass By Value" or "Pass By Reference" mean, read on…
This is what you expect. Great.
How about using the default value now:
There's no second argument when you call add_to_shopping_list()
this time. Therefore, the function creates an empty list and appends "Washing Up Liquid"
to it.
Again, this is the behaviour you expect.
So, using the or
expression to deal with the mutable default value in functions is cool, right?
Now Consider This…
Have a look at this scenario:
This scenario seems similar to the first one earlier, the one with the food_groceries
. You create a list called clothing_items
and you then pass it to the add_to_shopping_list()
function.
But now, although output
shows the expected result, the list clothing_items
is still empty.
Here's what's happening:
The list
clothing_items
is an empty list.You pass it to
add_to_shopping_list()
, soshopping_list
now refers to the same list asclothing_items
within the function.It's now the
or
expression's turn within the function,shopping_list = shopping_list or []
. But the identifier (name)shopping_list
now refers to the same list thatclothing_items
refers to. This is an empty list. Therefore, it's falsy……and since the first operand of the
or
expression is falsy, the expression evaluates to the second operand, which is also an empty list.But—and this is the key point—the
or
expression creates a new empty list rather than using the existing empty list (the one thatclothing_items
refers to).
So, you still have an empty list within your function, but it's not the same one you're expecting.
That's why output
and clothing_items
are different now. They're different lists. This didn't happen in your first example when you used food_groceries
. In that example, output
and food_groceries
both referred to the same list.
The standard way of solving the mutable default value problem doesn't face this issue:
This textbook approach to the mutable default value issue is less fancy, perhaps, but it works without surprises. It's also more readable, and that's important in Python!
Do you want to try video courses designed and delivered in the same style as these posts? You can get a free trial at The Python Coding Place, and you also get access to a members-only forum.
Photo by Dan Cristian Pădureț: https://www.pexels.com/photo/photo-of-multicolored-abstract-painting-1193743/
Code in this article uses Python 3.13
The code images used in this article are created using Snappify. [Affiliate link]
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
def add_to_shopping_list(item, shopping_list=None):
shopping_list = shopping_list or []
shopping_list.append(item)
return shopping_list
Code Block #2
food_groceries = ["Milk", "Eggs", "Bread"]
output = add_to_shopping_list("Chocolate", food_groceries)
output
# ['Milk', 'Eggs', 'Bread', 'Chocolate']
food_groceries
# ['Milk', 'Eggs', 'Bread', 'Chocolate']
Code Block #3
household_items = add_to_shopping_list("Washing Up Liquid")
household_items
# ['Washing Up Liquid']
Code Block #4
clothing_items = []
output = add_to_shopping_list("Shirt", clothing_items)
output
# ['Shirt']
clothing_items
# []
Code Block #5
def add_to_shopping_list(item, shopping_list=None):
if shopping_list is None:
shopping_list = []
shopping_list.append(item)
return shopping_list
clothing_items = []
output = add_to_shopping_list("Shirt", clothing_items)
output
# ['Shirt']
clothing_items
# ['Shirt']
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
Django Weblog
DSF member of the month - Jake Howard
For July 2025, we welcome Jake Howard as our DSF member of the month! ā
Jake actively shares his knowledge through blog posts and community talks. He is part of the Security Team Working Group and he created the DEP 14. He has been a DSF member since June 2024.
You can learn more about Jake by visiting Jake's website and his GitHub Profile.
Letās spend some time getting to know Jake better!
Can you tell us a little about yourself (hobbies, education, etc)
Iām Jake. Iām a Senior Systems Engineer at Torchbox, where Iāve been for a little over 4 years. āSystems Engineerā is a fairly loaded title, and means different things to different people. I like to describe it as doing everything technical to do with Software Engineering which isnāt Programming (Sysadmin, Devops, IT support, Security, Networking), but also doing a fair bit of Programming.
Most of my hobbies revolve around technology. Iām an avid self-hoster, running applications on servers both in āthe cloudā and in my house. Thereās been a server of some kind in my house for the last 10 years. Iām generally quite a private person, so I like to know whatās happening to my data. Since I started working remotely at the start of the 2020 pandemic, Iāve channeled some of this passion into posts on my website, with posts about all manner of things Iāve done from self-hosting to general software engineering.
Away from my desk (sort of), Iām a volunteer for Student Robotics, inspiring college students into STEM through competitive robotics (no, not quite like Robot Wars). In school, I was always the quiet one, but now I seem completely at home with public speaking, commentary and otherwise being in front of large crowds of people. I wish I knew the secret - Iād make millions!
My GitHub is also pretty active, with contributions all over the place (OpenZFS, Nebula VPN, Gitea, Plausible Analytics, OpenCV, Ansibleā¦).
Iām curious, where your nickname āRealOrangeOneā comes from?
Because a lot of life happens online (especially in the last 5 years), many people havenāt even seen pictures of me, let alone met me in person. I am not in fact a talking piece of fruit. For a while, I tried to stay anonymous, avoiding photos or videos of me on the internet. But since I discovered I enjoy public speaking, Iāve sort of given up on that (for the most part).
By now, Iām sure many people have speak. But, for those who donāt know: I, like my father before me, am ginger š„ (the hair colour, not the plant).
The exact specifics of how being ginger lead to āTheOrangeOneā are sadly lost to time. Iāve owned theorangeone.net
for well over a decade at this point. Unfortunately, itās not a particularly original nickname, and I have to be fast to claim it when signing up to new services. In some places (where I wasnāt fast enough) Iām forced to sub out āTheā for āRealā, which has lead to some confusions, but not too many. Canonically, I prefer āTheOrangeOneā, but as we all know, naming things is hard.
How did you start using Django?
Iāve been using Django since around the 1.8 release. My job at the time was at a Django development agency, so it was the first real Python framework Iād used. The first few weeks there was my first exposure to Django, pip, package management and collaborative software engineering - it was quite a lot to learn at once. I didnāt realise it at the time, but I was working working as a junior alongside a couple fairly well-known names in the Django community like Tom Christie (DRF, Starlette, HTTPX) and Jamie Matthews (django-readers, django-zen-queries). We mostly built single-page apps with React, so I learned Django and Django Rest Framework at the same time, which means I now often have to look back at the docs to remember how forms and templates work.
As for contributing to Django, that came much later. My first commit to Django was in May 2024. Having used Django for a while, and written plenty of packages, Iād never stopped to look at how upstream was developed. Around the time of DEP 14 kicking off, I needed to look a bit more at the inner workings of the Django project, to learn what was in store for me. When scrolling through Trac tickets, I found an interesting looking ticket, and got to work. At the time of writing, Iāve now closed 9 Trac tickets across 12 PRs, and some pretty cool features (simple block tags, better Accept header parsing, performance improvements to the URL router) now have my name on them (metaphorically speaking).
I wouldnāt call myself an āactiveā contributor, but I try and keep an eye on the tickets and forum threads which interest me the most, and chime in when I can.
What other framework do you know and if there is anything you would like to have in Django if you had magical powers?
Since itās the first framework I learned, and so far has done everything I need, Iāve mostly used Django. For a few smaller services, Iāve leaned more towards Starlette and AIOHTTP, but for anything even slightly large Iāve just used Django - since Iād end up recreating much of Django using the smaller frameworks anyway. A better (likely official) path for single-file Django (ie without some of the magic module handling) might help draw a few more people in and fill a few more of these āmicro-serviceā style use-cases.
Iām a class-based views person - I like the encapsulation and easy extension of base views. As with any opinion on the internet, Iām sure many people disagree with me, but to me itās just personal preference. Iām still surprised itās a pattern not seen by many other Python frameworks.
Following in the footsteps of Python, I often wonder if Django could also do with some dead battery removal (or at least extracting into separate packages). Django is a pretty big framework, and whilst the contrib apps are intended to be separate, they also require hooks and assumptions in other areas of the codebase. I might be wrong (it happens quite a lot), but I suspect some of those packages would be better suited externally, perhaps improving some developer momentum - and lightening the load for the Fellows. Djangoās sitemap and syndication (RSS) frameworks are 2 places I wish would get some more love.
Outside of Python, Iām a big fan of Rust (as cliche as it may be). Whilst Rust is a popular language, there isnāt really a āDjangoā like (batteries included) framework - itās all composing the pieces you need yourself. However, that doesnāt stop people being very productive with it. As a result, most of the frameworks have very generic interfaces, letting developers pass state around as needed, rather than trying to do everything themselves. Outside of the obvious static typing debate (which Iām in favour of), Iād love to see Django embrace some dependencies, especially if they bring some performance improvements. It may end up being a bad idea, but it might also help those who want to use Djangoās modules outside of Django.
Many years ago, I tried to be a polyglot - switching between different programming languages (and frameworks) to find new ways of working and match the problem to the correct solution. Now, Iāve settled mostly on Python and Rust. They fit my needs well, Iām very productive in them, and between the 2 thereās not much they canāt handle. Given my background, and the fact most sysadmin-y tools are written in it, Iām really not a fan of Go.
What projects are you working on now?
Over time, Iāve slowly stepped back from having big side projects - being a new dad sure takes up time and energy. Large projects ended up feeling too much like work outside of work, and I end up either getting distracted or bored. After work, I want to do something fun, not that seems like yet another job. Iām the kind of person who gets the sudden urge to research something interesting for an evening, dive in, then not think about it again for several weeks. Itās not the most productive way of doing things, which is why my posts are all over the place, but it doesnāt feel much like work for me - I lean heavily on what interests me at any time to drive what I want to do.
With that said, Iām currently in the process of rebuilding my website. Of course, both the current and new versions are built on Django, but the new build should be easier to maintain, faster, and hopefully wonāt need rewriting again in just a couple years. Most of my other projects have been small tools to make my home server that bit nicer.
Professionally, Iām not really a developer anymore. As a sysadmin (ish), much of my day-to-day doesnāt involve much programming. I spend much more of my time deploying, monitoring and administering Django applications than I do writing them. My main project at the moment is helping port a large Java / JS deployment over to Django and Wagtail, running on Kubernetes with some very high and interesting stability and scaling requirements. Since most of my professional live has been at software agencies, Iāve tended to bounce between different projects, rather than sitting on a single one. So Iām also supporting on a few other smaller projects as and when Iām needed.
Which Django libraries are your favorite (core or 3rd party)?
django-tasks, of course!
ā¦
Oh right, a serious answerā¦
I have to say, one of the most underrated modules in Django is django.utils
. Itās not as glamourous as the ORM, forms or cache, but itās a treasure trove of useful methods. I personally always like looking at the internal helper functions large frameworks use - see the problems theyāve had to solve time and time again. Whilst thereās not the same stability guarantees, Iāve definitely been helped out on a few occasions by some undocumented functions.
In that theme, Iām a fan of libraries which do one thing and do it well. I quite like small libraries which aim to solve a problem. Thereās definitely a line before that becomes a problem (anyone remember left-pad
?), but libraries which scope creep are often harder to work with than the more narrow-scoped ones, whilst the smaller ones just keep on working and making my life easier. For example, django-environ makes reading and parsing environment variables into settings really easy and clean, and django-decorator-include helps including other urlpatterns
whilst wrapping them in a decorator - particularly helpful for 3rd-party packageās URLs.
Finally, Iāve got a real soft-spot for whitenoise (and ServeStatic for ASGI users). Djangoās documentation deters people pretty hard from serving media and static files using Django - and rightly so in performance-critical environments. However, for most people, having to additionally maintain (and secure) nginx
is more maintenance than necessary. whitenoise
serves static files using Django directly, without any extra configuration, whilst also pre-compressing files for a nice performance boost. To me, itās such a universally-useful library, Iād love to see it it included in Django itself someday.
Iāll throw a bonus shout out for granian, a new (ish) WSGI / ASGI server written in Rust. gunicorn has a near monopoly on running Python apps in production, especially in the WSGI space, so itās nice to see a newcomer. granian isnāt always faster, but doing the HTTP handling in Rust (and using popular libraries to do it) can improve stability and throughput, without holding the GIL. Iāve not run anything in production with it yet, but Iāve been using it on personal projects for almost a year without issue.
What are the top three things in Django that you like?
Contrary to what Iāve already said, I actually like Djangoās batteries. Sure, thereās quite a few ādeadā ones in need of some cleaning up and TLC, but having most of what I need already installed makes me far more productive. I donāt need to think about how to render my form on the page, save the results as a model, or properly handle errors - everything ājust worksā, and works together. Sure, batteries have their downsides - it makes swapping them out rather difficult, but Iād rather ship my feature sooner than compare the trade-offs of different ORMs. The auto-reloading in django-tasks
is only around 8 lines of code thanks to django.utils.autoreload
being so easy to hook in to.
Secondly: Forms, but not for the reasons you might think. Most forms are created to take submissions from the user, validate them, then probably save them to a model. However, theyāre great as general data validation. Iāve written plenty of views with complex querystring requirements, and leaning on forms to validate them saves a lot of boilerplate code. Sure, pydantic
might be a bit faster and have more features, but given Iām already productive with django.forms
, and itās already installed and well understood by other developers in my team, I donāt feel the need to reach for something else.
Finally, I wouldnāt say itās quite a āfavouriteā, and itās well-known as being far-from-perfect, but Iāve got a real soft-spot for the Django Admin. It lets me focus on building the core of an application, rather than the internal interface - particularly when there are no strong requirements for it, or itās only going to be used by me and a few others. Since itās a fair raw view of the database by default, Iāve definitely been bitten by some less-than-restrictive permissions, but thereās generally all the hooks I need. I donāt like building frontends, so only needing to build 1 rather than 2 makes me a lot happier, especially if it comes with authentication, permissions, read-only views and a dark mode š!
How did you join the security team?
Iād love to say itās an interesting story, stroking my ego that I saved the day. But the reality is, as usual, far less glamorous.
As an engineer, Iāve tended towards 2 specialties: Security and Performance, which usually go hand-in-hand. In early 2023, I was invited to join the Wagtail CMS Security team after reporting and subsequently helping fix a memory exhaustion issue. I was already involved in all things security at Torchbox, especially our ISO-27001 certification, so I was already known when I submitted a vulnerability report.
Thibaud mentioned to me late last year that the project potentially looking for new members of the security team, to help with resourcing and some potential process improvements within the foundation. I naturally jumped at the opportunity - since the team is generally closed to new members and āfully-staffedā. After a few gentle reminders (heās a busy guy), I received a message from Sarah formally inviting me in March.
Since then, Iāve tried to review every report which came through, and helped author a few patches. A few reports even had to be raised upstream with Pythonās Security Response Team (PSRT). Itās been an interesting experience, and Iām looking forward to seeing how the team developers over the coming years.
Iām aware that you have created DEP 14 on the Background Workers, how the work is going so far? Do you need support from the community on anything?
DEP 14 (the proposal to add a native background workers API to Django) has been a really interesting journey. Iām beyond humbled to see the community interest behind it. When I started down this road, Iād only intended to start the conversations and help rally the community interest. Since then, and 6000 lines of code later, Iām mostly single-handedly writing a database-backed production-grade task system.
Right now, weāre at a bit of a cross-roads. Many of the foundational parts work, relatively well. The difficulty comes with the more complex features: Retries, dependencies, robust execution. Building a task system is easy - building a reliable one people want to actually use is incredibly difficult. If anyone out there is interested in getting involved, please do! Report issues, fix bugs, contribute to design discussions. Most of the APIs are based on what I think looks sensible. Software this large, pivotal and complex canāt be built in isolation - so it needs a diverse audience to ensure we (I) make the right decisions, and design an API people actually want to use that will last and scale for years to come.
The next challenge on my list to tackle is timeouts - a highly requested feature. It sounds simple, but the reality is far from it. Many of those challenges sparked the topic of my upcoming PyCon UK talk later this year.
Django is celebrating its 20th anniversary this month. Any nice story to share?
My personal highlight was DjangoCon Europe 2024 - my first DjangoCon. I ended up bringing the stereotypically grey British weather with me, but I had a great week chatting Django with some interesting people, and putting faces to the names and handles Iād seen online. After the talk announcing DEP 14 and background tasks, I was inundated with people voicing their support - many wondering how itād taken this long.
But personally, Iām more interested in whatās to come. Of course, thereās django-tasks
, but the next sets of releases are shaping up to be pretty interesting. Over the last 3-4 years or so, Iāve personally noticed a bit of a resurgence in peopleās appetites for change in Django. The 6.x Steering Council have a lot of interesting ideas, and clearly the community agree. People are happy with what Django can do now, but want to bring it a little more up-to-date - and are happy to put in the work to do it. Only a few weeks ago, django-csp was included in core, making it easier to make more secure applications. Iām sure thatās just the start. The fact people are still keen on working on a framework which just celebrated 20 years shows it must be doing something right!
Is there anything else youād like to say?
Iād like to thank whoever nominated me to be a DSF member in the first place. To this date, I have no idea who you are.
Beyond that, Iām just looking forward to seeing what comes of Django, and Python in general over the next few years.
Thank you for doing the interview, Jake !
August 01, 2025
Real Python
The Real Python Podcast ā Episode #259: Design Patterns That Don't Translate to Python
Do the design patterns learned in other programming languages translate to coding in Python? Christopher Trudeau is back on the show this week, bringing 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 ]
Zero to Mastery
[July 2025] Python Monthly Newsletter š
68th issue of Andrei Neagoie's must-read monthly Python Newsletter: Useless Design Patterns, Django turns 20, 330Ć faster Python, and much more. Read the full newsletter to get up-to-date with everything you need to know from last month.
Wingware
Wing Python IDE Version 11.0.3 - August 1, 2025
Wing Python IDE version 11.0.3 has been released. It improves Python code analysis, fixes problems debugging Django templates, fixes refactoring when the target file is in a hidden directory, and makes a number of other minor improvements.

Downloads
Wing 10 and earlier versions are not affected by installation of Wing 11 and may be installed and used independently. However, project files for Wing 10 and earlier are converted when opened by Wing 11 and should be saved under a new name, since Wing 11 projects cannot be opened by older versions of Wing.
New in Wing 11
Improved AI Assisted Development
Wing 11 improves the user interface for AI assisted development by introducing two separate tools AI Coder and AI Chat. AI Coder can be used to write, redesign, or extend code in the current editor. AI Chat can be used to ask about code or iterate in creating a design or new code without directly modifying the code in an editor.
Wing 11's AI assisted development features now support not just OpenAI but also Claude, Grok, Gemini, Perplexity, Mistral, Deepseek, and any other OpenAI completions API compatible AI provider.
This release also improves setting up AI request context, so that both automatically and manually selected and described context items may be paired with an AI request. AI request contexts can now be stored, optionally so they are shared by all projects, and may be used independently with different AI features.
AI requests can now also be stored in the current project or shared with all projects, and Wing comes preconfigured with a set of commonly used requests. In addition to changing code in the current editor, stored requests may create a new untitled file or run instead in AI Chat. Wing 11 also introduces options for changing code within an editor, including replacing code, commenting out code, or starting a diff/merge session to either accept or reject changes.
Wing 11 also supports using AI to generate commit messages based on the changes being committed to a revision control system.
You can now also configure multiple AI providers for easier access to different models.
For details see AI Assisted Development under Wing Manual in Wing 11's Help menu.
Package Management with uv
Wing Pro 11 adds support for the uv package manager in the New Project dialog and the Packages tool.
For details see Project Manager > Creating Projects > Creating Python Environments and Package Manager > Package Management with uv under Wing Manual in Wing 11's Help menu.
Improved Python Code Analysis
Wing 11 makes substantial improvements to Python code analysis, with better support for literals such as dicts and sets, parametrized type aliases, typing.Self, type of variables on the def or class line that declares them, generic classes with [...], __all__ in *.pyi files, subscripts in typing.Type and similar, type aliases, type hints in strings, type[...] and tuple[...], @functools.cached_property, base classes found also in .pyi files, and typing.Literal[...].
Updated Localizations
Wing 11 updates the German, French, and Russian localizations, and introduces a new experimental AI-generated Spanish localization. The Spanish localization and the new AI-generated strings in the French and Russian localizations may be accessed with the new User Interface > Include AI Translated Strings preference.
Improved diff/merge
Wing Pro 11 adds floating buttons directly between the editors to make navigating differences and merging easier, allows undoing previously merged changes, and does a better job managing scratch buffers, scroll locking, and sizing of merged ranges.
For details see Difference and Merge under Wing Manual in Wing 11's Help menu.
Other Minor Features and Improvements
Wing 11 also improves the custom key binding assignment user interface, adds a Files > Auto-Save Files When Wing Loses Focus preference, warns immediately when opening a project with an invalid Python Executable configuration, allows clearing recent menus, expands the set of available special environment variables for project configuration, and makes a number of other bug fixes and usability improvements.
Changes and Incompatibilities
Since Wing 11 replaced the AI tool with AI Coder and AI Chat, and AI configuration is completely different than in Wing 10, you will need to reconfigure your AI integration manually in Wing 11. This is done with Manage AI Providers in the AI menu. After adding the first provider configuration, Wing will set that provider as the default. You can switch between providers with Switch to Provider in the AI menu.
If you have questions, please don't hesitate to contact us at support@wingware.com.
Matt Layman
Python and AI workflow with LangGraph
In this stream, I worked on a personal AI workflow that I’m building using LangGraph. I discussed human-in-the-loop and how to bring a person into the workflow process.
HoloViz
Plotting made easy with hvPlot: 0.12 release
meejah.ca
ShWiM: peer-to-peer terminal sharing
SHell WIth Me combines magic-wormhole and tty-share for e2ee, p2p terminal sharing
July 31, 2025
Python Morsels
Nested functions in Python
Functions in Python can be defined within another function.
A function defined within a function
Python's functions can be defined pretty much anywhere.
You can even define a function inside a function:
def greet_me(name="friend"):
def greet():
print("Hello", name)
greet()
When we call this greet_me
function, it defines a greet
a function and then calls that function:
>>> greet_me()
Hello friend
Note that the inner function is allowed to use the name
from the outer function.
A function returned from another function
Instead of calling our inner ā¦
Read the full article: https://www.pythonmorsels.com/nested-functions/
Django Weblog
Djangonaut Space is looking for contributors to be mentors
Hello Django š Universe!
š°ļøā This is Djangonaut Space phoning home about Session 5! We're recruiting technical mentors (Navigators) to join our next šstellarš mission.
š©āš We are looking for people who regularly contribute to Django or a Django related package, that want to mentor others. Our next session will be Oct-Nov.
š Come join us and be a cosmic contributor! Express your interest to be a mentor here.
š Want to learn more about what it means to be a Navigator:
š¤ Interested people will have to complete a 30 minute meet & greet type interview with organizers.
ā If you're interested in applying to be a Djangonaut, applications will open and close in September (dates to be determined). The latest information will be posted on our site, djangonaut.space. Please follow our social media accounts or subscribe to our newsletter for announcements.
āļø We'll see you around the cosmos!
Djangonaut Space session organizers
PyCharm
The Bazel Plugin for IntelliJ IDEA Is Now Generally Available!
After much anticipation, we are finally ready to announce the general availability (GA) of the new Bazel plugin for IntelliJ IDEA, PyCharm, and GoLand ā now developed by JetBrains! After months of focused development and valuable feedback from our EAP users, we’re officially launching our revamped Bazel experience.
While we’ve been shipping updates regularly, the leap to our 2025.2 GA release marks a major milestone. Even though our primary focus for this release was on creating the best experience we can for Java, Kotlin, and Scala developers, we also brought support for the Python and Go ecosystems, and we will continue to maintain and improve it in the coming releases.
If you’re migrating from the previous plugin originally released by Google, you’ll notice a more straightforward workflow that aligns with the standard JetBrains IDE experience you expect from other build tool integrations such as Maven and Gradle. Now, let’s dive into what’s new!
Key features in 2025.2

Bazel Query in Action
- Go is a go. We’re officially rolling out support for Go. You can now import your Go targets in Bazel projects into both IntelliJ IDEA (with the Go plugin) and GoLand. This brings the full IDE experience you rely on: code highlighting, completion, navigation, and the ability to run, debug, and get coverage for your tests.
- Built-in Bazel Query tool window: Go beyond sync and build with Bazel queries integrated directly into your IDE via their own dedicated tool window. Craft your queries with syntax completion and a helpful UI for flags to explore your project’s dependency graph without ever leaving the editor.
- Dramatically faster indexing: We’ve optimized indexing to get you to your code faster. You can now use the
import_depth
andimport_ijars
settings in your.bazelproject
file to prevent the indexing of deep transitive dependencies and index only header jars instead of full jars. Whatās more, only the files directly referenced in your.bazelproject
view are fully indexed for code intelligence, which can slash indexing times and memory usage in large projects with many auxiliary files.
New plugin, new user experience
Back in December, we publicly announced the EAP (Early Access Program) version of our new plugin and defined what it would take to release it into GA, with an overview of the main differences between the original plugin and the new one.
Hereās a quick recap for those moving from the older plugin: We’ve smoothed out the rough edges to make Bazel feel like a natural part of the IDE.
- Simplified project import: The old import wizard is a thing of the past. Now, simply open a directory containing your
MODULE.bazel
orWORKSPACE
file. For more control, you can open a specific.bazelproject
view file. If you manage a large monorepo, you can provide a default template for your team by checking in a template attools/intellij/.managed.bazelproject
. - Redesigned UI elements: The Bazel tool window is now your central hub for actions like resyncing your project (with a Build and Resync option for generating sources) and keeping track of targets in your working set. We’ve also added a widget listing all targets the currently opened file belongs to. It allows you to run actions on these targets (build / test / jump to BUILD file / copy target label)
- Reworked target mapping for JVM projects: A core improvement is the new internal representation for JVM targets, which mirrors the actual Bazel graph. This fundamental change enables more accurate highlighting, more accurate completions and more reliable refactoring.
Improvements since 2025.1
Windows compatibility
We understand that development doesn’t just happen on one OS. Thatās why we worked on making our plugin compatible with Microsoft Windows, bringing most of the feature set to our Windows-based users.
Enhanced Bazel configuration support
We believe editing your build files should be as easy as editing your source code, which is why we’ve improved the user experience for all Bazel-related configuration files.
Starlark (.bzl
, BUILD
)

Starlark Quick Documentation
- Quick documentation for Starlark rules: Hover over a Starlark rule or function to see its documentation directly in the editor. You’ll also get documentation as you type, guiding you through available parameters.
- Automatic formatting: If you have
buildifier
on yourPATH
, the plugin will now automatically format your Starlark files on save
Bazel module configuration file (MODULE.bazel
)
- Intelligent editing: The
MODULE.bazel
editor now offers smart completions for arguments and displays documentation as you edit.
Bazel project view file (.bazelproject
)

.bazelproject view highlighting and completions
- Guided editing: Get completions for section names and known values. The editor will now highlight completely unsupported sections as errors and sections that are supported in the old plugin originally by Google (but not in the new one) as warnings.
- Manage directories from the Project view file tree: You can now right-click a directory in the project tree to add or remove it from your
.bazelproject
file, thus loading or unloading that directory in IntelliJ.
Bazelisk configuration file (.bazelversion
):
- Stay up to date: The editor will now highlight outdated Bazel versions specified in your
.bazelversion
file and offer a quick-fix to update to the latest release.
Language ecosystem enhancements
- JVM:
- The underlying project model mapping has been further improved, resulting in better performance during sync and more reliable refactorings for targets where glob patterns match the whole directory.
- Scala:
- The Bazel plugin now respects the
scalacopts
parameter in yourscala_*
targets, which unlocks Scala 3 highlighting features with the-Xsource:3
flag. At the same time, we’ve updated the Scala plugin to provide native integration with the Bazel plugin out of the box.
- The Bazel plugin now respects the
- Python:
- Run from the gutter:
py_test
andpy_binary
targets now get the familiar green Run arrow in the editor gutter. - Improved dependency resolution: Python dependencies are now resolved correctly, enabling code navigation and eliminating false error highlighting.
- Interpreter from
MODULE.bazel
: The plugin now sets the Python interpreter based on what is defined inMODULE.bazel
. This includes support for hermetic toolchains downloaded byrules_python
ā meaning you don’t need to have Python installed locally on your machine. - Debugging: You can now attach the debugger to
py_test
targets.
- Run from the gutter:
What happens to the Bazel plugin by Google?
The Bazel for IntelliJ plugin (also known as IJwB) by Google is being deprecated. Google has transferred the code ownership and maintenance to JetBrains. We will keep providing compatibility updates for new IntelliJ versions and critical fixes only throughout the year of 2025, but will be fully deprecating it in 2026. All our development effort for IntelliJ IDEA, GoLand, and PyCharm is now focused on the new plugin.
The Bazel for CLion plugin (CLwB) has also been transferred to JetBrains, and will continue to be actively developed. Learn more in the post Enhancing Bazel Support for CLion on the CLion Blog.
Got feedback? Weāre listening!
We’re committed to making this the best Bazel experience possible. Please report any issues, ideas, or improvements straight to our issue tracker.
Fixed the problem yourself? We accept PRs on our hirschgarten repository.
You’ll also find us on the Bazel Community Slack, in the #intellij
channel.
Happy building!
Bazel Plugin Release: General Availability
Daniel Roy Greenfeld
Unpack for keyword arguments
Previously I wrote a TIL on how to better type annotate callables with *args
and **kwargs
- in essence you ignore the container and worry just about the content of the container. This makes sense, as *args
are a tuple and **kwargs
keys are strings.
Here's an example of that in action:
>>> def func(*args, **kwargs):
... print(f'{args=}')
... print(f'{kwargs=}')
args=(1, 2, 3)
kwargs={'one': 1, 'two': 2}
In fact, if you try to force **kwargs
to accept a non-string type Python stops you with a TypeError:
>>> func(**{1:2})
Traceback (most recent call last):
File "<python-input-9>", line 1, in <module>
func(**{1:2})
~~~~^^^^^^^^^
TypeError: keywords must be strings
This is all great, but what if you want your keyword arguments to consistently accept a pattern of arguments? So this passes type checks:
from typing import TypedDict, Unpack
class Cheese(TypedDict):
name: str
price: int
def func(**cheese: Unpack[Cheese]) -> None:
print(cheese)
Let's try it out:
>>> func(name='Paski Sir', price=30)
{'name': 'Paski Sir', 'price': 30}
Works great! Now let's break it by forgetting a keyword argument:
>>> func(name='Paski Sir')
{'name': 'Paski Sir'}
What? How about adding an extra keyword argument and replacing the int
with a float
:
>>> func(name='Paski Sir', price=30.5, country='Croatia')
{'name': 'Paski Sir', 'price': 30.5, 'country': 'Croatia'}
Still no errors? What gives? The answer is that type annotations are for type checkers, and don't catch during runtime. See the [note at the top of the core Python docs on typing]:
Note The Python runtime does not enforce function and variable type annotations. They can be used by third party tools such as type checkers, IDEs, linters, etc.
For those times when we do need runtime evaluations of types, we lean on built-ins like isinstance
and issubclass
, which are quite seperate from type hints and annotations.
Thanks to the astute Luke Plant for pointing out Unpack
to me and sending me down a quite pleasant rabbit hole.
July 30, 2025
Test and Code
236: Git Tips for Testing - Adam Johnson
In this episode, host Brian Okken and guest Adam Johnson explore essential Git features, highlighted by Adam's updated book, "Boost Your Git DX."Ā
Key topics includeĀ
- "cherry picking" for selective commits
- "git stash" for managing in-progress work
- "git diff", and specifically its `--name-only` flag, which provides a streamlined method for developers to identify which files have changed, which can be used to determine which tests need to be run
- "git bisect" for efficiently pinpointing bugs.Ā
This conversation offers valuable strategies for developers at any skill level to enhance their Git proficiency and optimize their coding workflows.
Links:
- Boost Your Git DX - Adam's book
Help support the show AND learn pytest:Ā
- The Complete pytest course is now a bundle, with each part available separately.
- pytest Primary Power teaches the super powers of pytest that you need to learn to use pytest effectively.
- Using pytest with Projects has lots of "when you need it" sections like debugging failed tests, mocking, testing strategy, and CI
- Then pytest Booster Rockets can help with advanced parametrization and building plugins.
- Whether you need to get started with pytest today, or want to power up your pytest skills, PythonTest has a course for you.
Real Python
Python's asyncio: A Hands-On Walkthrough
Pythonās asyncio
library enables you to write concurrent code using the async
and await
keywords. The core building blocks of async I/O in Python are awaitable objectsāmost often coroutinesāthat an event loop schedules and executes asynchronously. This programming model lets you efficiently manage multiple I/O-bound tasks within a single thread of execution.
In this tutorial, youāll learn how Python asyncio
works, how to define and run coroutines, and when to use asynchronous programming for better performance in applications that perform I/O-bound tasks.
By the end of this tutorial, youāll understand that:
- Pythonās
asyncio
provides a framework for writing single-threaded concurrent code using coroutines, event loops, and non-blocking I/O operations. - For I/O-bound tasks, async I/O can often outperform multithreadingāespecially when managing a large number of concurrent tasksābecause it avoids the overhead of thread management.
- You should use
asyncio
when your application spends significant time waiting on I/O operations, such as network requests or file access, and you want to run many of these tasks concurrently without creating extra threads or processes.
Through hands-on examples, youāll gain the practical skills to write efficient Python code using asyncio
that scales gracefully with increasing I/O demands.
Get Your Code: Click here to download the free sample code that youāll use to learn about async I/O in Python.
Take the Quiz: Test your knowledge with our interactive āPython's asyncio: A Hands-On Walkthroughā quiz. Youāll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Python's asyncio: A Hands-On WalkthroughTest your knowledge of `asyncio` concurrency with this quiz that covers coroutines, event loops, and efficient I/O-bound task management.
A First Look at Async I/O
Before exploring asyncio
, itās worth taking a moment to compare async I/O with other concurrency models to see how it fits into Pythonās broader, sometimes dizzying, landscape. Here are some essential concepts to start with:
- Parallelism consists of executing multiple operations at the same time.
- Multiprocessing is a means of achieving parallelism that entails spreading tasks over a computerās central processing unit (CPU) cores. Multiprocessing is well-suited for CPU-bound tasks, such as tightly bound
for
loops and mathematical computations. - Concurrency is a slightly broader term than parallelism, suggesting that multiple tasks have the ability to run in an overlapping manner. Concurrency doesnāt necessarily imply parallelism.
- Threading is a concurrent execution model in which multiple threads take turns executing tasks. A single process can contain multiple threads. Pythonās relationship with threading is complicated due to the global interpreter lock (GIL), but thatās beyond the scope of this tutorial.
Threading is good for I/O-bound tasks. An I/O-bound job is dominated by a lot of waiting on input/output (I/O) to complete, while a CPU-bound task is characterized by the computerās cores continually working hard from start to finish.
The Python standard library has offered longstanding support for these models through its multiprocessing
, concurrent.futures
, and threading
packages.
Now itās time to add a new member to the mix. In recent years, a separate model has been more comprehensively built into CPython: asynchronous I/O, commonly called async I/O. This model is enabled through the standard libraryās asyncio
package and the async
and await
keywords.
Note: Async I/O isnāt a new concept. It exists ināor is being built intoāother languages such as Go, C#, and Rust.
The asyncio
package is billed by the Python documentation as a library to write concurrent code. However, async I/O isnāt threading or multiprocessing. Itās not built on top of either of these.
Async I/O is a single-threaded, single-process technique that uses cooperative multitasking. Async I/O gives a feeling of concurrency despite using a single thread in a single process. Coroutinesāor coro for shortāare a central feature of async I/O and can be scheduled concurrently, but theyāre not inherently concurrent.
To reiterate, async I/O is a model of concurrent programming, but itās not parallelism. Itās more closely aligned with threading than with multiprocessing, but itās different from both and is a standalone member of the concurrency ecosystem.
That leaves one more term. What does it mean for something to be asynchronous? This isnāt a rigorous definition, but for the purposes of this tutorial, you can think of two key properties:
- Asynchronous routines can pause their execution while waiting for a result and allow other routines to run in the meantime.
- Asynchronous code facilitates the concurrent execution of tasks by coordinating asynchronous routines.
Hereās a diagram that puts it all together. The white terms represent concepts, and the green terms represent the ways theyāre implemented:

For a thorough exploration of threading versus multiprocessing versus async I/O, pause here and check out the Speed Up Your Python Program With Concurrency tutorial. For now, youāll focus on async I/O.
Async I/O Explained
Async I/O may seem counterintuitive and paradoxical at first. How does something that facilitates concurrent code use a single thread in a single CPU core? Miguel Grinbergās PyCon talk explains everything quite beautifully:
Chess master Judit PolgƔr hosts a chess exhibition in which she plays multiple amateur players. She has two ways of conducting the exhibition: synchronously and asynchronously.
Assumptions:
- 24 opponents
- Judit makes each chess move in 5 seconds
- Opponents each take 55 seconds to make a move
- Games average 30 pair-moves (60 moves total)
Synchronous version: Judit plays one game at a time, never two at the same time, until the game is complete. Each game takes (55 + 5) * 30 == 1800 seconds, or 30 minutes. The entire exhibition takes 24 * 30 == 720 minutes, or 12 hours.
Asynchronous version: Judit moves from table to table, making one move at each table. She leaves the table and lets the opponent make their next move during the wait time. One move on all 24 games takes Judit 24 * 5 == 120 seconds, or 2 minutes. The entire exhibition is now cut down to 120 * 30 == 3600 seconds, or just 1 hour. (Source)
Read the full article at https://realpython.com/async-io-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 ]
Python Insider
Python 3.14 release candidate 1 is go!
Itās the first 3.14 release candidate!
https://www.python.org/downloads/release/python-3140rc1/
This is the first release candidate of Python 3.14
This release, 3.14.0rc1, is the penultimate release preview. Entering the release candidate phase, only reviewed code changes which are clear bug fixes are allowed between this release candidate and the final release. The second candidate (and the last planned release preview) is scheduled for Tuesday, 2025-08-26, while the official release of 3.14.0 is scheduled for Tuesday, 2025-10-07.
There will be no ABI changes from this point forward in the 3.14 series, and the goal is that there will be as few code changes as possible.
Call to action
We strongly encourage maintainers of third-party Python projects to prepare their projects for 3.14 during this phase, and where necessary publish Python 3.14 wheels on PyPI to be ready for the final release of 3.14.0, and to help other projects do their own testing. Any binary wheels built against Python 3.14.0rc1 will work with future versions of Python 3.14. As always, report any issues to the Python bug tracker.
Please keep in mind that this is a preview release and while itās as close to the final release as we can get it, its use is not recommended for production environments.
Core developers: time to work on documentation now
- Are all your changes properly documented?
- Are they mentioned in Whatās New?
- Did you notice other changes you know of to have insufficient documentation?
Major new features of the 3.14 series, compared to 3.13
Some of the major new features and changes in Python 3.14 are:
New features
- PEP 779: Free-threaded Python is officially supported
- PEP 649: The evaluation of annotations is now deferred, improving the semantics of using annotations.
- PEP 750: Template string literals (t-strings) for custom string processing, using the familiar syntax of f-strings.
- PEP 734: Multiple interpreters in the stdlib.
- PEP
784: A new module
compression.zstd
providing support for the Zstandard compression algorithm. - PEP
758:
except
andexcept*
expressions may now omit the brackets. - Syntax highlighting in PyREPL, and support for color in unittest, argparse, json and calendar CLIs.
- PEP 768: A zero-overhead external debugger interface for CPython.
- UUID
versions 6-8 are now supported by the
uuid
module, and generation of versions 3-5 are up to 40% faster. - PEP
765: Disallow
return
/break
/continue
that exit afinally
block. - PEP 741: An improved C API for configuring Python.
- A new type of interpreter. For certain newer compilers, this interpreter provides significantly better performance. Opt-in for now, requires building from source.
- Improved error messages.
- Builtin implementation of HMAC with formally verified code from the HACL* project.
- A new command-line interface to inspect running Python processes using asynchronous tasks.
- The pdb module now supports remote attaching to a running Python process.
(Hey, fellow core developer, if a feature you find important is missing from this list, let Hugo know.)
For more details on the changes to Python 3.14, see Whatās new in Python 3.14. The next pre-release of Python 3.14 will be the final release candidate, 3.14.0rc2, scheduled for 2025-08-26.
Build changes
- PEP 761: Python 3.14 and onwards no longer provides PGP signatures for release artifacts. Instead, Sigstore is recommended for verifiers.
- Official macOS and Windows release binaries include an experimental JIT compiler.
Incompatible changes, removals and new deprecations
- Incompatible changes
- Python removals and deprecations
- C API removals and deprecations
- Overview of all pending deprecations
Python install manager
The installer we offer for Windows is being replaced by our new install manager, which can be installed from the Windows Store or from its download page. See our documentation for more information. The JSON file available for download below contains the list of all the installable packages available as part of this release, including file URLs and hashes, but is not required to install the latest release. The traditional installer will remain available throughout the 3.14 and 3.15 releases.
More resources
- Online documentation
- PEP 745, 3.14 Release Schedule
- Report bugs at github.com/python/cpython/issues
- Help fund Python and its community
And now for something completely different
Today, 22nd July, is Pi Approximation Day, because 22/7 is a common approximation of Ļ and closer to Ļ than 3.14.
22/7 is a Diophantine approximation, named after Diophantus of Alexandria (3rd century CE), which is a way of estimating a real number as a ratio of two integers. 22/7 has been known since antiquity; Archimedes (3rd century BCE) wrote the first known proof that 22/7 overestimates Ļ by comparing 96-sided polygons to the circle it circumscribes.
Another approximation is 355/113. In Chinese mathematics, 22/7 and 355/113 are respectively known as Yuelü (ēŗ¦ē; yuÄlĒ; āapproximate ratioā) and Milü (åÆē; mƬlĒ; āclose ratioā).
Happy Pi Approximation Day!
Enjoy the new release
Thanks to all of the many volunteers who help make Python Development and these releases possible! Please consider supporting our efforts by volunteering yourself or through organisation contributions to the Python Software Foundation.
Regards from a Helsinki heatwave after an excellent EuroPython,
Your release team,
Hugo van Kemenade
Ned Deily
Steve Dower
Åukasz Langa
Mike Driscoll
Creating a Simple XML Editor in Your Terminal with Python and Textual
Several years ago, I created an XML editor with the wxPython GUI toolkit called Boomslang. I recently thought it would be fun to port that code to Textual so I could have an XML viewer and editor in my terminal as well.
In this article, you will learn how that experiment went and see the results. Here is a quick outline of what you will cover:
- Get the packages you will need
- Create the main UI
- Creating the edit XML screen
- The add node screen
- Adding an XML preview screen
- Creating file browser and warning screens
- Creating the file save screen
Let’s get started!
Getting the Dependencies
You will need Textual to be able to run the application detailed in this tutorial. You will also need lxml, which is a super fast XML parsing package. You can install Textual using pip or uv. You can probably use uv with lxml as well, but pip definitely works.
Here’s an example using pip to install both packages:
python -m pip install textual lxml
Once pip has finished installing Textual and the lxml package and all its dependencies, you will be ready to continue!
Creating the Main UI
The first step in creating the user interface is figuring out what it should look like. Here is the original Boomslang user interface that was created using wxPython:
You want to create something similar to this UI, but in your terminal. Open up your favorite Python IDE and create a new file called boomslang.py
and then enter the following code into it:
from pathlib import Path from .edit_xml_screen import EditXMLScreen from .file_browser_screen import FileBrowser from textual import on from textual.app import App, ComposeResult from textual.containers import Horizontal, Vertical from textual.widgets import Button, Header, Footer, OptionList class BoomslangXML(App): BINDINGS = [ ("ctrl+o", "open", "Open XML File"), ] CSS_PATH = "main.tcss" def __init__(self) -> None: super().__init__() self.title = "Boomslang XML" self.recent_files_path = Path(__file__).absolute().parent / "recent_files.txt" self.app_selected_file: Path | None = None self.current_recent_file: Path | None = None def compose(self) -> ComposeResult: self.recent_files = OptionList("", id="recent_files") self.recent_files.border_title = "Recent Files" yield Header() yield self.recent_files yield Vertical( Horizontal( Button("Open XML File", id="open_xml_file", variant="primary"), Button("Open Recent", id="open_recent_file", variant="warning"), id="button_row", ) ) yield Footer() def on_mount(self) -> None: self.update_recent_files_ui() def action_open(self) -> None: self.push_screen(FileBrowser()) def on_file_browser_selected(self, message: FileBrowser.Selected) -> None: path = message.path if path.suffix.lower() == ".xml": self.update_recent_files_on_disk(path) self.push_screen(EditXMLScreen(path)) else: self.notify("Please choose an XML File!", severity="error", title="Error") @on(Button.Pressed, "#open_xml_file") def on_open_xml_file(self) -> None: self.push_screen(FileBrowser()) @on(Button.Pressed, "#open_recent_file") def on_open_recent_file(self) -> None: if self.current_recent_file is not None and self.current_recent_file.exists(): self.push_screen(EditXMLScreen(self.current_recent_file)) @on(OptionList.OptionSelected, "#recent_files") def on_recent_files_selected(self, event: OptionList.OptionSelected) -> None: self.current_recent_file = Path(event.option.prompt) def update_recent_files_ui(self) -> None: if self.recent_files_path.exists(): self.recent_files.clear_options() files = self.recent_files_path.read_text() for file in files.split("\n"): self.recent_files.add_option(file.strip()) def update_recent_files_on_disk(self, path: Path) -> None: if path.exists() and self.recent_files_path.exists(): recent_files = self.recent_files_path.read_text() if str(path) in recent_files: return with open(self.recent_files_path, mode="a") as f: f.write(str(path) + "\n") self.update_recent_files_ui() elif not self.recent_files_path.exists(): with open(self.recent_files_path, mode="a") as f: f.write(str(path) + "\n") def main() -> None: app = BoomslangXML() app.run() if __name__ == "__main__": main()
That’s a good chunk of code, but it’s still less than a hundred lines. You will go over it in smaller chunks though. You can start with this first chunk:
from pathlib import Path from .edit_xml_screen import EditXMLScreen from .file_browser_screen import FileBrowser from textual import on from textual.app import App, ComposeResult from textual.containers import Horizontal, Vertical from textual.widgets import Button, Header, Footer, OptionList class BoomslangXML(App): BINDINGS = [ ("ctrl+o", "open", "Open XML File"), ] CSS_PATH = "main.tcss" def __init__(self) -> None: super().__init__() self.title = "Boomslang XML" self.recent_files_path = Path(__file__).absolute().parent / "recent_files.txt" self.app_selected_file: Path | None = None self.current_recent_file: Path | None = None
You need a few imports to make your code work. The first import comes from Python itself and gives your code the ability to work with file paths. The next two are for a couple of small custom files you will create later on. The rest of the imports are from Textual and provide everything you need to make a nice little Textual application.
Next, you create the BoomslangXML
class where you set up a keyboard binding and set which CSS file you will be using for styling your application.
The __init__()
method sets the following:
- The title of the application
- The recent files path, which contains all the files you have recently opened
- The currently selected file or None
- The current recent file (i.e. the one you have open at the moment) or None
Now you are ready to create the main UI:
def compose(self) -> ComposeResult: self.recent_files = OptionList("", id="recent_files") self.recent_files.border_title = "Recent Files" yield Header() yield self.recent_files yield Vertical( Horizontal( Button("Open XML File", id="open_xml_file", variant="primary"), Button("Open Recent", id="open_recent_file", variant="warning"), id="button_row", ) ) yield Footer()
To create your user interface, you need a small number of widgets:
- A header to identify the name of the application
- An OptionList which contains the recently opened files, if any, that the user can reload
- A button to load a new XML file
- A button to load from the selected recent file
- A footer to show the application’s keyboard shortcuts
Next, you will write a few event handlers:
def on_mount(self) -> None: self.update_recent_files_ui() def action_open(self) -> None: self.push_screen(FileBrowser()) def on_file_browser_selected(self, message: FileBrowser.Selected) -> None: path = message.path if path.suffix.lower() == ".xml": self.update_recent_files_on_disk(path) self.push_screen(EditXMLScreen(path)) else: self.notify("Please choose an XML File!", severity="error", title="Error")
The code above contains the logic for three event handlers:
on_mount()
– After the application loads, it will update the OptionList by reading the text file that contains paths to the recent files.action_open()
– A keyboard shortcut action that gets called when the user presses CTRL+O. It will then show a file browser to the user so they can pick an XML file to load.on_file_browser_selected()
– Called when the user picks an XML file from the file browser and closes the file browser. If the file is an XML file, you will reload the screen to allow XML editing. Otherwise, you will notify the user to choose an XML file.
The next chunk of code is for three more event handlers:
@on(Button.Pressed, "#open_xml_file") def on_open_xml_file(self) -> None: self.push_screen(FileBrowser()) @on(Button.Pressed, "#open_recent_file") def on_open_recent_file(self) -> None: if self.current_recent_file is not None and self.current_recent_file.exists(): self.push_screen(EditXMLScreen(self.current_recent_file)) @on(OptionList.OptionSelected, "#recent_files") def on_recent_files_selected(self, event: OptionList.OptionSelected) -> None: self.current_recent_file = Path(event.option.prompt)
These event handlers use Textual’s handy @on
decorator, which allows you to bind the event to a specific widget or widgets.
on_open_xml_file()
– If the user presses the “Open XML File” button, this method is called and it will show the file browser.on_open_recent_file()
– If the user presses the “Open Recent” button, this method gets called and will load the selected recent file.on_recent_files_selected()
– When the user selects a recent file in the OptionList widget, this method gets called and sets thecurrent_recent_file
variable.
You only have two more methods to go over. The first is for updating the recent files UI:
def update_recent_files_ui(self) -> None: if self.recent_files_path.exists(): self.recent_files.clear_options() files = self.recent_files_path.read_text() for file in files.split("\n"): self.recent_files.add_option(file.strip())
Remember, this method gets called by on_mount()
and it will update the OptionList, if the file exists. The first thing this code will do is clear the OptionList in preparation for updating it. Then you will read the text from the file and loop over each path in that file.
As you loop over the paths, you add them to the OptionList. That’s it! You now have a recent files list that the user can choose from.
The last method to write is for updating the recent files text file:
def update_recent_files_on_disk(self, path: Path) -> None: if path.exists() and self.recent_files_path.exists(): recent_files = self.recent_files_path.read_text() if str(path) in recent_files: return with open(self.recent_files_path, mode="a") as f: f.write(str(path) + "\n") self.update_recent_files_ui() elif not self.recent_files_path.exists(): with open(self.recent_files_path, mode="a") as f: f.write(str(path) + "\n")
When the user opens a new XML file, you want to add that file to the recent file list on disk so that the next time the user opens your application, you can show the user the recent files. This is a nice way to make loading previous files much easier.
The code above will verify that the file still exists and that your recent files file also exists. Assuming that they do, you will check to see if the current XML file is already in the recent files file. If it is, you don’t want to add it again, so you return.
Otherwise, you open the recent files file in append mode, add the new file to disk and update the UI.
If the recent files file does not exist, you create it here and add the new path.
Here are the last few lines of code to add:
def main() -> None: app = BoomslangXML() app.run() if __name__ == "__main__": main()
You create a main()
function to create the Textual application object and run it. You do this primarily for making the application runnable by uv, Python’s fastest package installer and resolver.
Now you’re ready you move on and add some CSS styling to your UI.
Your XML editor doesn’t require extensive styling. In fact, there is nothing wrong with being minimalistic.
Open up your favorite IDE or text editor and create a new file named main.tcss
and then add the following code:
BoomslangXML { #button_row { align: center middle; } Horizontal{ height: auto; } OptionList { border: solid green; } Button { margin: 1; } }
Here you center the button row on your screen. You also set the Horizontal
container’s height to auto, which tells Textual to make the container fit its contents. You also add a border to your OptionList
and a margin to your buttons.
The XML editor screen is fairly complex, so that’s what you will learn about next.
Creating the Edit XML Screen
The XML editor screen is more complex than the main screen of your application and contains almost twice as many lines of code. But that’s to be expected when you realize that most of your logic will reside here.
As before, you will start out by writing the full code and then going over it piece-by-piece. Open up your Python IDE and create a new file named edit_xml_screen.py
and then enter the following code:
import lxml.etree as ET import tempfile from pathlib import Path from .add_node_screen import AddNodeScreen from .preview_xml_screen import PreviewXMLScreen from textual import on from textual.app import ComposeResult from textual.containers import Horizontal, Vertical, VerticalScroll from textual.screen import ModalScreen from textual.widgets import Footer, Header, Input, Tree from textual.widgets._tree import TreeNode class DataInput(Input): """ Create a variant of the Input widget that stores data """ def __init__(self, xml_obj: ET.Element, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.xml_obj = xml_obj class EditXMLScreen(ModalScreen): BINDINGS = [ ("ctrl+s", "save", "Save"), ("ctrl+a", "add_node", "Add Node"), ("p", "preview", "Preview"), ("escape", "esc", "Exit dialog"), ] CSS_PATH = "edit_xml_screens.tcss" def __init__(self, xml_path: Path, *args, **kwargs): super().__init__(*args, **kwargs) self.xml_tree = ET.parse(xml_path) self.expanded = {} self.selected_tree_node: None | TreeNode = None def compose(self) -> ComposeResult: xml_root = self.xml_tree.getroot() self.expanded[id(xml_root)] = "" yield Header() yield Horizontal( Vertical(Tree("No Data Loaded", id="xml_tree"), id="left_pane"), VerticalScroll(id="right_pane"), id="main_ui_container", ) yield Footer() def on_mount(self) -> None: self.load_tree() @on(Tree.NodeExpanded) def on_tree_node_expanded(self, event: Tree.NodeExpanded) -> None: """ When a tree node is expanded, parse the newly shown leaves and make them expandable, if necessary. """ xml_obj = event.node.data if id(xml_obj) not in self.expanded and xml_obj is not None: for top_level_item in xml_obj.getchildren(): child = event.node.add_leaf(top_level_item.tag, data=top_level_item) if top_level_item.getchildren(): child.allow_expand = True else: child.allow_expand = False self.expanded[id(xml_obj)] = "" @on(Tree.NodeSelected) def on_tree_node_selected(self, event: Tree.NodeSelected) -> None: """ When a node in the tree control is selected, update the right pane to show the data in the XML, if any """ xml_obj = event.node.data right_pane = self.query_one("#right_pane", VerticalScroll) right_pane.remove_children() self.selected_tree_node = event.node if xml_obj is not None: for child in xml_obj.getchildren(): if child.getchildren(): continue text = child.text if child.text else "" data_input = DataInput(child, text) data_input.border_title = child.tag container = Horizontal(data_input) right_pane.mount(container) else: # XML object has no children, so just show the tag and text if getattr(xml_obj, "tag") and getattr(xml_obj, "text"): if xml_obj.getchildren() == []: data_input = DataInput(xml_obj, xml_obj.text) data_input.border_title = xml_obj.tag container = Horizontal(data_input) right_pane.mount(container) @on(Input.Changed) def on_input_changed(self, event: Input.Changed) -> None: """ When an XML element changes, update the XML object """ xml_obj = event.input.xml_obj # self.notify(f"{xml_obj.text} is changed to new value: {event.input.value}") xml_obj.text = event.input.value def action_esc(self) -> None: """ Close the dialog when the user presses ESC """ self.dismiss() def action_add_node(self) -> None: """ Add another node to the XML tree and the UI """ # Show dialog and use callback to update XML and UI def add_node(result: tuple[str, str] | None) -> None: if result is not None: node_name, node_value = result self.update_xml_tree(node_name, node_value) self.app.push_screen(AddNodeScreen(), add_node) def action_preview(self) -> None: temp_directory = Path(tempfile.gettempdir()) xml_path = temp_directory / "temp.xml" self.xml_tree.write(xml_path) self.app.push_screen(PreviewXMLScreen(xml_path)) def action_save(self) -> None: self.xml_tree.write(r"C:\Temp\books.xml") self.notify("Saved!") def load_tree(self) -> None: """ Load the XML tree UI with data parsed from the XML file """ tree = self.query_one("#xml_tree", Tree) xml_root = self.xml_tree.getroot() self.expanded[id(xml_root)] = "" tree.reset(xml_root.tag) tree.root.expand() # If the root has children, add them if xml_root.getchildren(): for top_level_item in xml_root.getchildren(): child = tree.root.add(top_level_item.tag, data=top_level_item) if top_level_item.getchildren(): child.allow_expand = True else: child.allow_expand = False def update_tree_nodes(self, node_name: str, node: ET.SubElement) -> None: """ When adding a new node, update the UI Tree element to reflect the new element added """ child = self.selected_tree_node.add(node_name, data=node) child.allow_expand = False def update_xml_tree(self, node_name: str, node_value: str) -> None: """ When adding a new node, update the XML object with the new element """ element = ET.SubElement(self.selected_tree_node.data, node_name) element.text = node_value self.update_tree_nodes(node_name, element)
Phew! That seems like a lot of code if you are new to coding, but a hundred and seventy lines of code or so really isn’t very much. Most applications take thousands of lines of code.
Just the same, breaking the code down into smaller chunks will aid in your understanding of what’s going on.
With that in mind, here’s the first chunk:
import lxml.etree as ET import tempfile from pathlib import Path from .add_node_screen import AddNodeScreen from .preview_xml_screen import PreviewXMLScreen from textual import on from textual.app import ComposeResult from textual.containers import Horizontal, Vertical, VerticalScroll from textual.screen import ModalScreen from textual.widgets import Footer, Header, Input, Tree from textual.widgets._tree import TreeNode
You have move imports here than you did in the main UI file. Here’s a brief overview:
- You import lxml to make parsing and editing XML easy.
- You use Python’s
tempfile
module to create a temporary file for viewing the XML. - The
pathlib
module is used the same way as before. - You have a couple of custom Textual screens that you will need to code up and import.
- The last six lines are all Textual imports for making this editor screen work.
The next step is to subclass the Input
widget in such a way that it will store XML element data:
class DataInput(Input): """ Create a variant of the Input widget that stores data """ def __init__(self, xml_obj: ET.Element, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.xml_obj = xml_obj
Here you pass in an XML object and store it off in an instance variable. You will need this to make editing and displaying the XML easy.
The second class you create is the EditXMLScreen
:
class EditXMLScreen(ModalScreen): BINDINGS = [ ("ctrl+s", "save", "Save"), ("ctrl+a", "add_node", "Add Node"), ("p", "preview", "Preview"), ("escape", "esc", "Exit dialog"), ] CSS_PATH = "edit_xml_screens.tcss" def __init__(self, xml_path: Path, *args, **kwargs): super().__init__(*args, **kwargs) self.xml_tree = ET.parse(xml_path) self.expanded = {} self.selected_tree_node: None | TreeNode = None
The EditXMLScreen
is a new screen that holds your XML editor. Here you add four keyboard bindings, a CSS file path and the __init__()
method.
Your initialization method is used to create an lxml Element Tree instance. You also create an empty dictionary of expanded tree widgets and the selected tree node instance variable, which is set to None
.
Now you’re ready to create your user interface:
def compose(self) -> ComposeResult: xml_root = self.xml_tree.getroot() self.expanded[id(xml_root)] = "" yield Header() yield Horizontal( Vertical(Tree("No Data Loaded", id="xml_tree"), id="left_pane"), VerticalScroll(id="right_pane"), id="main_ui_container", ) yield Footer() def on_mount(self) -> None: self.load_tree()
Fortunately, the user interface needed for editing XML is fairly straightforward:
- You create a new header to add a new title to the screen.
- You use a horizontally-oriented container to hold your widgets.
- Inside of the container, you have a tree control that holds the DOM of the XML on the left.
- On the right, you have a vertical scrolling container.
- Finally, you have a footer
You also set up the first item in your “expanded” dictionary, which is the root node from the XML.
Now you can write your first event handler for this class:
@on(Tree.NodeExpanded) def on_tree_node_expanded(self, event: Tree.NodeExpanded) -> None: """ When a tree node is expanded, parse the newly shown leaves and make them expandable, if necessary. """ xml_obj = event.node.data if id(xml_obj) not in self.expanded and xml_obj is not None: for top_level_item in xml_obj.getchildren(): child = event.node.add_leaf(top_level_item.tag, data=top_level_item) if top_level_item.getchildren(): child.allow_expand = True else: child.allow_expand = False self.expanded[id(xml_obj)] = ""
When the user expands a node in the tree control, the on_tree_node_expanded()
method will get called. You will extract the node’s data, if it has any. Assuming that there is data, you will then loop over any child nodes that are present.
For each child node, you will add a new leaf to the tree control. You check to see if the child has children too and set the allow_expand
flag accordingly. At the end of the code, you add then XML object to your dictionary.
The next method you need to write is an event handler for when a tree node is selected:
@on(Tree.NodeSelected) def on_tree_node_selected(self, event: Tree.NodeSelected) -> None: """ When a node in the tree control is selected, update the right pane to show the data in the XML, if any """ xml_obj = event.node.data right_pane = self.query_one("#right_pane", VerticalScroll) right_pane.remove_children() self.selected_tree_node = event.node if xml_obj is not None: for child in xml_obj.getchildren(): if child.getchildren(): continue text = child.text if child.text else "" data_input = DataInput(child, text) data_input.border_title = child.tag container = Horizontal(data_input) right_pane.mount(container) else: # XML object has no children, so just show the tag and text if getattr(xml_obj, "tag") and getattr(xml_obj, "text"): if xml_obj.getchildren() == []: data_input = DataInput(xml_obj, xml_obj.text) data_input.border_title = xml_obj.tag container = Horizontal(data_input) right_pane.mount(container)
Wben the user selects a node in your tree, you need to update the righthand pane with the node’s contents. To do that, you once again extract the node’s data, if it has any. If it does have data, you loop over its children and update the right hand pane’s UI. This entails grabbing the XML node’s tags and values and adding a series of horizontal widgets to the scrollable container that makes up the right pane of your UI.
If the XML object has no children, you can simply show the top level node’s tag and value, if it has any.
The next two methods you will write are as follows:
@on(Input.Changed) def on_input_changed(self, event: Input.Changed) -> None: """ When an XML element changes, update the XML object """ xml_obj = event.input.xml_obj xml_obj.text = event.input.value def on_save_file_dialog_dismissed(self, xml_path: str) -> None: """ Save the file to the selected location """ if not Path(xml_path).exists(): self.xml_tree.write(xml_path) self.notify(f"Saved to: {xml_path}")
The on_input_changed()
method deals with Input
widgets which are your special DataInput
widgets. Whenever they are edited, you want to grab the XML object from the event and update the XML tag’s value accordingly. That way, the XML will always be up-to-date if the user decides they want to save it.
You can also add an auto-save feature which would also use the latest XML object when it is saving, if you wanted to.
The second method here, on_save_file_dialog_dismissed()
, is called when the user dismisses the save dialog that is opened when the user pressesĀ CTRL+S. Here you check to see if the file already exists. If not, you create it. You could spend some time adding another dialog here that warns that a file exists and gives the option to the user whether or not to overwrite it.
Anyway, your next step is to write the keyboard shortcut action methods. There are four keyboard shortcuts that you need to create actions for.
Here they are:
def action_esc(self) -> None: """ Close the dialog when the user presses ESC """ self.dismiss() def action_add_node(self) -> None: """ Add another node to the XML tree and the UI """ # Show dialog and use callback to update XML and UI def add_node(result: tuple[str, str] | None) -> None: if result is not None: node_name, node_value = result self.update_xml_tree(node_name, node_value) self.app.push_screen(AddNodeScreen(), add_node) def action_preview(self) -> None: temp_directory = Path(tempfile.gettempdir()) xml_path = temp_directory / "temp.xml" self.xml_tree.write(xml_path) self.app.push_screen(PreviewXMLScreen(xml_path)) def action_save(self) -> None: self.app.push_screen(SaveFileDialog(), self.on_save_file_dialog_dismissed)
The four keyboard shortcut event handlers are:
action_esc()
– Called when the user pressed the “Esc” key. Exits the dialog.action_add_node()
– Called when the user presses CTRL+A. Opens theAddNodeScreen
. If the user adds new data, theadd_node()
callback is called, which will then callupdate_xml_tree()
to update the UI with the new information.action_preview()
– Called when the user presses the “p” key. Creates a temporary file with the current contents of the XML object. Then opens a new screen that allows the user to view the XML as a kind of preview.action_save
– Called when the user pressesĀ CTRL+S.
The next method you will need to write is called load_tree()
:
def load_tree(self) -> None: """ Load the XML tree UI with data parsed from the XML file """ tree = self.query_one("#xml_tree", Tree) xml_root = self.xml_tree.getroot() self.expanded[id(xml_root)] = "" tree.reset(xml_root.tag) tree.root.expand() # If the root has children, add them if xml_root.getchildren(): for top_level_item in xml_root.getchildren(): child = tree.root.add(top_level_item.tag, data=top_level_item) if top_level_item.getchildren(): child.allow_expand = True else: child.allow_expand = False
The method above will grab the Tree
widget and the XML’s root element and then load the tree widget with the data. You check if the XML root object has any children (which most do) and then loop over the children, adding them to the tree widget.
You only have two more methods to write. Here they are:
def update_tree_nodes(self, node_name: str, node: ET.SubElement) -> None: """ When adding a new node, update the UI Tree element to reflect the new element added """ child = self.selected_tree_node.add(node_name, data=node) child.allow_expand = False def update_xml_tree(self, node_name: str, node_value: str) -> None: """ When adding a new node, update the XML object with the new element """ element = ET.SubElement(self.selected_tree_node.data, node_name) element.text = node_value self.update_tree_nodes(node_name, element)
These two methods are short and sweet:
update_tree_nodes()
– When the user adds a new node, you call this method which will update the node in the tree widget as needed.update_xml_tree()
– When a node is added, update the XML object and then call the UI updater method above.
The last piece of code you need to write is the CSS for this screen. Open up a text editor and create a new file called edit_xml_screens.tcss and then add the following code:
EditXMLScreen { Input { border: solid gold; margin: 1; height: auto; } Button { align: center middle; } Horizontal { margin: 1; height: auto; } }
This CSS is similar to the other CSS file. In this case, you set the Input
widget’s height to auto. You also set the margin and border for that widget. For the buttons, you tell Textual to center all of them. Finally, you also set the margin and height of the horizontal container, just like you did in the other CSS file.
Now you are ready to learn about the add node screen!
The Add Node Screen
When the user wants to add a new node to the XML, you will show an “add node screen”. This screen allows the user to enter a node (i.e., tag) name and value. The screen will then pass that new data to the callback which will update the XML object and the user interface. You have already seen that code in the previous section.
To get started, open up a new file named add_node_screen.py
and enter the following code:
from textual import on from textual.app import ComposeResult from textual.containers import Horizontal, Vertical from textual.screen import ModalScreen from textual.widgets import Button, Header, Footer, Input class AddNodeScreen(ModalScreen): BINDINGS = [ ("escape", "esc", "Exit dialog"), ] CSS_PATH = "add_node_screen.tcss" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.title = "Add New Node" def compose(self) -> ComposeResult: self.node_name = Input(id="node_name") self.node_name.border_title = "Node Name" self.node_value = Input(id="node_value") self.node_value.border_title = "Node Value" yield Vertical( Header(), self.node_name, self.node_value, Horizontal( Button("Save Node", variant="primary", id="save_node"), Button("Cancel", variant="warning", id="cancel_node"), ), Footer(), id="add_node_screen_ui", ) @on(Button.Pressed, "#save_node") def on_save(self) -> None: self.dismiss((self.node_name.value, self.node_value.value)) @on(Button.Pressed, "#cancel_node") def on_cancel(self) -> None: self.dismiss() def action_esc(self) -> None: """ Close the dialog when the user presses ESC """ self.dismiss()
Following is an overview of each method of the code above:
__init__()
– Sets the title of the screen.compose()
– Creates the user interface, which is made up of twoInput
widgets, a “Save” button, and a “Cancel” button.on_save()
-Called when the user presses the “Save” button. This will save the data entered by the user into the two inputs, if any.on_cancel()
– Called when the user presses the “Cancel” button. If pressed, the screen exits without saving.action_esc()
– Called when the user presses the “Esc” key. If pressed, the screen exits without saving.
That code is concise and straightforward.
Next, open up a text editor or use your IDE to create a file named add_node_screen.tcss
which will contain the following CSS:
AddNodeScreen { align: center middle; background: $primary 30%; #add_node_screen_ui { width: 80%; height: 40%; border: thick $background 70%; content-align: center middle; margin: 2; } Input { border: solid gold; margin: 1; height: auto; } Button { margin: 1; } Horizontal{ height: auto; align: center middle; } }
Your CSS functions as a way to quickly style individual widgets or groups of widgets. Here you set it up to make the screen a bit smaller than the screen underneath it (80% x 40%) so it looks like a dialog.
You set the border, height, and margin on your inputs. You add a margin around your buttons to keep them slightly apart. Finally, you add a height and alignment to the container.
You can try tweaking all of this to see how it changes the look and feel of the screen. It’s a fun way to explore, and you can do this with any of the screens you create.
The next screen to create is the XML preview screen.
Adding an XML Preview Screen
The XML Preview screen allows the user to check that the XML looks correct before they save it. Textual makes creating a preview screen short and sweet.
Open up your Python IDE and create a new file named preview_xml_screen.py
and then enter the following code into it:
from textual import on from textual.app import ComposeResult from textual.containers import Center, Vertical from textual.screen import ModalScreen from textual.widgets import Button, Header, TextArea class PreviewXMLScreen(ModalScreen): CSS_PATH = "preview_xml_screen.tcss" def __init__(self, xml_file_path: str, *args: tuple, **kwargs: dict) -> None: super().__init__(*args, **kwargs) self.xml_file_path = xml_file_path self.title = "Preview XML" def compose(self) -> ComposeResult: with open(self.xml_file_path) as xml_file: xml = xml_file.read() text_area = TextArea(xml) text_area.language = "xml" yield Header() yield Vertical( text_area, Center(Button("Exit Preview", id="exit_preview", variant="primary")), id="exit_preview_ui", ) @on(Button.Pressed, "#exit_preview") def on_exit_preview(self, event: Button.Pressed) -> None: self.dismiss()
There’s not a lot here, so you will go over the highlights like you did in the previous section:
__init__()
– Initializes a couple of instance variables:xml_file_path
– Which is a temporary file pathtitle
– The title of the screen
compose()
– The UI is created here. You open the XML file and read it in. Then you load the XML into aTextArea
widget. Finally, you tell Textual to use a header, the text area widget and an exit button for your interface.on_exit_preview()
– Called when the user presses the “Exit Preview” button. As the name implies, this exits the screen.
The last step is to apply a little CSS. Create a new file named preview_xml_screen.tcss
and add the following snippet to it:
PreviewXMLScreen { Button { margin: 1; } }
All this CSS does is add a margin to the button, which makes the UI look a little nicer.
There are three more screens yet to write. The first couple of screens you will create are the file browser and warning screens.
Creating the File Browser and Warning Screens
The file browser is what the user will use to find an XML file that they want to open. It is also nice to have a screen you can use for warnings, so you will create that as well.
For now, you will call this file file_browser_screen.py
but you are welcome to separate these two screens into different files. The first half of the file will contain the imports and the WarningScreen
class.
Here is that first half:
from pathlib import Path from textual import on from textual.app import ComposeResult from textual.containers import Center, Grid, Vertical from textual.message import Message from textual.screen import Screen from textual.widgets import Button, DirectoryTree, Footer, Label, Header class WarningScreen(Screen): """ Creates a pop-up Screen that displays a warning message to the user """ def __init__(self, warning_message: str) -> None: super().__init__() self.warning_message = warning_message def compose(self) -> ComposeResult: """ Create the UI in the Warning Screen """ yield Grid( Label(self.warning_message, id="warning_msg"), Button("OK", variant="primary", id="ok_warning"), id="warning_dialog", ) def on_button_pressed(self, event: Button.Pressed) -> None: """ Event handler for when the OK button - dismisses the screen """ self.dismiss() event.stop()
The warning screen is made up of two widgets: a label that contains the warning message and an “OK” button. You also add a method to respond to the buton being pressed. You dismiss the screen here and stop the event from propagating up to the parent.
The next class you need to add to this file is the FileBrowser
class:
class FileBrowser(Screen): BINDINGS = [ ("escape", "esc", "Exit dialog"), ] CSS_PATH = "file_browser_screen.tcss" class Selected(Message): """ File selected message """ def __init__(self, path: Path) -> None: self.path = path super().__init__() def __init__(self) -> None: super().__init__() self.selected_file = Path("") self.title = "Load XML Files" def compose(self) -> ComposeResult: yield Vertical( Header(), DirectoryTree("/"), Center( Button("Load File", variant="primary", id="load_file"), ), id="file_browser_dialog", ) @on(DirectoryTree.FileSelected) def on_file_selected(self, event: DirectoryTree.FileSelected) -> None: """ Called when the FileSelected Message is emitted from the DirectoryTree """ self.selected_file = event.path def on_button_pressed(self, event: Button.Pressed) -> None: """ Event handler for when the load file button is pressed """ event.stop() if self.selected_file.suffix.lower() != ".xml" and self.selected_file.is_file(): self.app.push_screen(WarningScreen("ERROR: You must choose a XML file!")) return self.post_message(self.Selected(self.selected_file)) self.dismiss() def action_esc(self) -> None: """ Close the dialog when the user presses ESC """ self.dismiss()
The FileBrowser
class is more complicated because it does a lot more than the warning screen does. Here’s a listing of the methods:
__init__()
– Initializes the currently selected file to an empty path and sets the title for the screen.compose()
– Creates the UI. This UI has a header, aDirectoryTree
for browsing files and a button for loading the currently selected file.on_file_selected()
– When the user selected a file in the directory tree, you grab the path and set theselected_file
instance variable.on_button_pressed()
– When the user presses the “Load File” button, you check if the selected file is the correct file type. If not, you should a warning screen. If the file is an XML file, then you post a custom message and close the screen.action_esc()
– Called when the user presses theEsc
key. Closes the screen.
The last item to write is your CSS file. As you might expect, you should name it file_browser_screen.tcss
. Then put the following CSS inside of the file:
FileBrowser { #file_browser_dialog { width: 80%; height: 50%; border: thick $background 70%; content-align: center middle; margin: 2; border: solid green; } Button { margin: 1; content-align: center middle; } }
The CSS code here should look pretty familiar to you. All you are doing is making the screen look like a dialog and then adding a margin and centering the button.
The last step is to create the file save screen.
Creating the File Save Screen
The file save screen is similar to the file browser screen with the main difference being that you are supplying a new file name that you want to use to save your XML file to.
Open your Python IDE and create a new file called save_file_dialog.py
and then enter the following code:
from pathlib import Path from textual import on from textual.app import ComposeResult from textual.containers import Vertical from textual.screen import Screen from textual.widgets import Button, DirectoryTree, Footer, Header, Input, Label class SaveFileDialog(Screen): CSS_PATH = "save_file_dialog.tcss" def __init__(self) -> None: super().__init__() self.title = "Save File" self.root = "/" def compose(self) -> ComposeResult: yield Vertical( Header(), Label(f"Folder name: {self.root}", id="folder"), DirectoryTree("/"), Input(placeholder="filename.txt", id="filename"), Button("Save File", variant="primary", id="save_file"), id="save_dialog", ) def on_mount(self) -> None: """ Focus the input widget so the user can name the file """ self.query_one("#filename").focus() def on_button_pressed(self, event: Button.Pressed) -> None: """ Event handler for when the load file button is pressed """ event.stop() filename = self.query_one("#filename").value full_path = Path(self.root) / filename self.dismiss(f"{full_path}") @on(DirectoryTree.DirectorySelected) def on_directory_selection(self, event: DirectoryTree.DirectorySelected) -> None: """ Called when the DirectorySelected message is emitted from the DirectoryTree """ self.root = event.path self.query_one("#folder").update(f"Folder name: {event.path}")
The save file dialog code is currently less than fifty lines of code. Here is a breakdown of that code:
__init__()
– Sets the title of the screen and the default root folder.compose()
– Creates the user interface, which consists of a header, a label (the root), the directory tree widget, an input for specifying the file name, and a “Save File” button.on_mount()
– Called automatically by Textual after thecompose()
method. Sets the input widget as the focus.on_button_pressed()
– Called when the user presses the “Save File” button. Grabs the filename and then create the full path using the root + filename. Finally, you send that full path back to the callback function viadismiss()
.on_directory_selection()
– Called when the user selects a directory. Updates theroot
variable to the selected path as well as updates the label so the user knows which path is selected.
The last item you need to write is the CSS file for this dialog. You will need to name the file save_file_dialog.tcss
and then add this code:
SaveFileDialog { #save_dialog { width: 80%; height: 50%; border: thick $background 70%; content-align: center middle; margin: 2; border: solid green; } Button { margin: 1; content-align: center middle; } }
The CSS code above is almost identical to the CSS you used for the file browser code.
When you run the TUI, you should see something like the following demo GIF:
Wrapping Up
You have now created a basic XML editor and viewer using Python and Textual. There are lots of little improvements that you can add to this code. However, those updates are up to you to make.
Have fun working with Textual and create something new or contribute to a neat Textual project yourself!
Get the Code
The code in this tutorial is based on version 0.2.0 of BoomslangXML TUI. You can download the code from GitHub or from the following links:
The post Creating a Simple XML Editor in Your Terminal with Python and Textual appeared first on Mouse Vs Python.
Quansight Labs Blog
Learning from accessibility work
Years of accessibility work around Jupyter and thoughts on how to survive it in your own projects.
Armin Ronacher
Agentic Coding Things That Didnāt Work
Using Claude Code and other agentic coding tools has become all the rage. Not only is it getting millions of downloads, but these tools are also gaining features that help streamline workflows. As you know, I got very excited about agentic coding in May, and I’ve tried many of the new features that have been added. I’ve spent considerable time exploring everything on my plate.
But oddly enough, very little of what I attempted I ended up sticking with. Most of my attempts didn’t last, and I thought it might be interesting to share what didn’t work. This doesn’t mean these approaches won’t work or are bad ideas; it just means I didn’t manage to make them work. Maybe there’s something to learn from these failures for others.
Rules of Automation
The best way to think about the approach that I use is:
- I only automate things that I do regularly.
- If I create an automation for something that I do regularly, but then I stop using the automation, I consider it a failed automation and I delete it.
Non-working automations turn out to be quite common. Either I can’t get myself to use them, I forget about them, or I end up fine-tuning them endlessly. For me, deleting a failed workflow helper is crucial. You don’t want unused Claude commands cluttering your workspace and confusing others.
So I end up doing the simplest thing possible most of the time: just talk to the machine more, give it more context, keep the audio input going, and dump my train of thought into the prompt. And that is 95% of my workflow. The rest might be good use of copy/paste.
Slash Commands
Slash commands allow you to preload prompts to have them readily available in a session. I expected these to be more useful than they ended up being. I do use them, but many of the ones that I added I ended up never using.
There are some limitations with slash commands that make them less useful than they could be. One limitation is that there’s only one way to pass arguments, and it’s unstructured. This proves suboptimal in practice for my uses. Another issue I keep running into with Claude Code is that if you do use a slash command, the argument to the slash command for some reason does not support file-based autocomplete.
To make them work better, I often ask Claude to use the current Git state to determine which files to operate on. For instance, I have a command in this blog that fixes grammar mistakes. It operates almost entirely from the current git status context because providing filenames explicitly is tedious without autocomplete.
Here is one of the few slash commands I actually do use:
## Context
- git status: !`git status`
- Explicitly mentioned file to fix: "$ARGUMENTS"
## Your task
Based on the above information, I want you to edit the mentioned file or files
for grammar mistakes. Make a backup (eg: change file.md to file.md.bak) so I
can diff it later. If the backup file already exists, delete it.
If a blog post was explicitly provided, edit that; otherwise, edit the ones
that have pending changes or are untracked.
My workflow now assumes that Claude can determine which files I mean from the Git status virtually every time, making explicit arguments largely unnecessary.
Here are some of the many slash commands that I built at one point but ended up not using:
/fix-bug
: I had a command that instructed Claude to fix bugs by pulling issues from GitHub and adding extra context. But I saw no meaningful improvement over simply mentioning the GitHub issue URL and voicing my thoughts about how to fix it./commit
: I tried getting Claude to write good commit messages, but they never matched my style. I stopped using this command, though I haven’t given up on the idea entirely./add-tests
: I really hoped this would work. My idea was to have Claude skip tests during development, then use an elaborate reusable prompt to generate them properly at the end. But this approach wasn’t consistently better than automatic test generation, which I’m still not satisfied with overall./fix-nits
: I had a command to fix linting issues and run formatters. I stopped using it because it never became muscle memory, and Claude already knows how to do this. I can just tell it “fix lint” in the CLAUDE.md file without needing a slash command./next-todo
: I track small items in a to-do.md file and had a command to pull the next item and work on it. Even here, workflow automation didn’t help much. I use this command far less than expected.
So if I’m using fewer slash commands, what am I doing instead?
- Speech-to-text. Cannot stress this enough but talking to the machine means you’re more likely to share more about what you want it to do.
- I maintain some basic prompts and context for copy-pasting at the end or the beginning of what I entered.
Copy/paste is really, really useful because of how fuzzy LLMs are. For instance, I maintain link collections that I paste in when needed. Sometimes I fetch files proactively, drop them into a git-ignored folder, and mention them. It’s simple, easy, and effective. You still need to be somewhat selective to avoid polluting your context too much, but compared to having it spelunk in the wrong places, more text doesn’t harm as much.
Hooks
I tried hard to make hooks work, but I haven’t seen any efficiency gains from them yet. I think part of the problem is that I use yolo mode. I wish hooks could actually manipulate what gets executed. The only way to guide Claude today is through denies, which don’t work in yolo mode. For instance, I tried using hooks to make it use uv instead of regular Python, but I was unable to do so. Instead, I ended up preloading executables on the PATH that override the default ones, steering Claude toward the right tools.
For instance, this is really my hack for making it use uv run python
instead
of python
more reliably:
#!/bin/sh
echo "This project uses uv, please use 'uv run python' instead."
exit 1
I really just have a bunch of these in .claude/interceptors
and preload that
folder onto PATH
before launching Claude:
CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR=1 \
PATH="`pwd`/.claude/interceptors:${PATH}" \
claude --dangerously-skip-permissions
I also found it hard to hook into the right moment. I wish I could run formatters at the end of a long edit session. Currently, you must run formatters after each Edit tool operation, which often forces Claude to re-read files, wasting context. Even with the Edit tool hook, I’m not sure if I’m going to keep using it.
I’m actually really curious whether people manage to get good use out of hooks. I’ve seen some discussions on Twitter that suggest there are some really good ways of making them work, but I just went with much simpler solutions instead.
Claude Print Mode
I was initially very bullish on Claude’s print mode. I tried hard to have Claude generate scripts that used print mode internally. For instance, I had it create a mock data loading script ā mostly deterministic code with a small inference component to generate test data using Claude Code.
The challenge is achieving reliability, which hasn’t worked well for me yet. Print mode is slow and difficult to debug. So I use it far less than I’d like, despite loving the concept of mostly deterministic scripts with small inference components. Whether using the Claude SDK or the command-line print flag, I haven’t achieved the results I hoped for.
I’m drawn to Print Mode because inference is too much like a slot machine. Many programming tasks are actually quite rigid and deterministic. We love linters and formatters because they’re unambiguous. Anything we can fully automate, we should. Using an LLM for tasks that don’t require inference is the wrong approach in my book.
That’s what makes print mode appealing. If only it worked better. Use an LLM for the commit message, but regular scripts for the commit and gh pr commands. Make mock data loading 90% deterministic with only 10% inference.
I still use it, but I see more potential than I am currently leveraging.
Sub Tasks and Sub Agents
I use the task tool frequently for basic parallelization and context isolation. Anthropic recently launched an agents feature meant to streamline this process, but I haven’t found it easier to use.
Sub-tasks and sub-agents enable parallelism, but you must be careful. Tasks that don’t parallelize well ā especially those mixing reads and writes ā create chaos. Outside of investigative tasks, I don’t get good results. While sub-agents should preserve context better, I often get better results by starting new sessions, writing thoughts to Markdown files, or even switching to o3 in the chat interface.
Does It Help?
What’s interesting about workflow automation is that without rigorous rules that you consistently follow as a developer, simply taking time to talk to the machine and give clear instructions outperforms elaborate pre-written prompts.
For instance, I don’t use emojis or commit prefixes. I don’t enforce templates for pull requests either. As a result, there’s less structure for me to teach the machine.
I also lack the time and motivation to thoroughly evaluate all my created workflows. This prevents me from gaining confidence in their value.
Context engineering and management remain major challenges. Despite my efforts to help agents pull the right data from various files and commands, they don’t yet succeed reliably. They pull in too much or too little. Long sessions lead to forgotten context from the beginning. Whether done manually or with slash commands, the results feel too random. It’s hard enough with ad-hoc approaches, but static prompts and commands make it even harder.
The rule I have now is that if I do want to automate something, I must have done it a few times already, and then I evaluate whether the agent gets any better results through my automation. There’s no exact science to it, but I mostly measure that right now by letting it do the same task three times and looking at the variance manually as measured by: would I accept the result.
Keeping The Brain On
Forcing myself to evaluate the automation has another benefit: I’m less likely to just blindly assume it helps me.
Because there is a big hidden risk with automation through LLMs: it encourages mental disengagement. When you stop thinking like an engineer, quality drops, time gets wasted and you don’t understand and learn. LLMs are already bad enough as they are, but whenever I lean in on automation I notice that it becomes even easier to disengage. I tend to overestimate the agent’s capabilities with time. There are real dragons there!
You can still review things as they land, but it becomes increasingly harder to do so later. While LLMs are reducing the cost of refactoring, the cost doesn’t drop to zero, and regressions are common.