skip to navigation
skip to content

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 ]

August 04, 2025 12:00 PM UTC

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 ]

August 04, 2025 12:00 PM UTC

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 ]

August 04, 2025 12:00 PM UTC

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 ]

August 04, 2025 12:00 PM UTC


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:

Thank you the Code of Conduct team responded to the issue reported.

August 04, 2025 07:00 AM UTC


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.

:man_teacher: 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

Giving Back

A portion of the participation fee will be donated to the:

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

Read full topic

August 04, 2025 06:00 AM UTC

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:

All code blocks are available in text format at the end of this article • #1 • The code images used in this article are created using Snappify. [Affiliate link]

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:

Let's try it out to confirm this is how the function works.

First, try with an existing list:

#2

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:

#3

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?

Subscribe now

Now Consider This…

Have a look at this scenario:

#4

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:

  1. The list clothing_items is an empty list.

  2. You pass it to add_to_shopping_list(), so shopping_list now refers to the same list as clothing_items within the function.

  3. 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 that clothing_items refers to. This is an empty list. Therefore, it's falsy…

  4. …and since the first operand of the or expression is falsy, the expression evaluates to the second operand, which is also an empty list.

  5. But—and this is the key point—the or expression creates a new empty list rather than using the existing empty list (the one that clothing_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.


Support The Python Coding Stack


The standard way of solving the mutable default value problem doesn't face this issue:

#5

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.

Try Out The Python Coding Place

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.

Support The Python Coding Stack


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

August 03, 2025 07:54 PM UTC


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 03, 2025 01:20 PM UTC

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 ]

August 01, 2025 12:00 PM UTC


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.

August 01, 2025 10:00 AM UTC


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.

Wing 11 Screen Shot

Downloads

Be sure to Check for Updates in Wing's Help menu after downloading, to make sure that you have the latest hot fixes.

Wing Pro 11.0.3

Wing Personal 11.0.3

Wing 101 11.0.3

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.

August 01, 2025 01:00 AM UTC


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.

August 01, 2025 12:00 AM UTC


HoloViz

Plotting made easy with hvPlot: 0.12 release

August 01, 2025 12:00 AM UTC


meejah.ca

ShWiM: peer-to-peer terminal sharing

SHell WIth Me combines magic-wormhole and tty-share for e2ee, p2p terminal sharing

August 01, 2025 12:00 AM UTC

July 31, 2025


Python Morsels

Nested functions in Python

Functions in Python can be defined within another function.

Table of contents

  1. A function defined within a function
  2. A function returned from another function
  3. The enclosing scope
  4. Closures with nested functions
  5. Why nest functions within functions?
  6. Decorators involve nested functions
  7. Nested functions are possible in Python

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/

July 31, 2025 03:30 PM UTC


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

July 31, 2025 02:34 PM UTC


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

Bazel Query in Action

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.

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

Starlark Quick Documentation

Bazel module configuration file (MODULE.bazel)

Bazel project view file (.bazelproject)

.bazelproject view highlighting and completions

.bazelproject view highlighting and completions

Bazelisk configuration file (.bazelversion):

Language ecosystem enhancements

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!

July 31, 2025 01:40 PM UTC

Bazel Plugin Release: General Availability

July 31, 2025 10:20 AM UTC


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 31, 2025 12:12 AM UTC

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Ā 

This conversation offers valuable strategies for developers at any skill level to enhance their Git proficiency and optimize their coding workflows.


Links:


Help support the show AND learn pytest:Ā 

ā˜… Support this podcast on Patreon ā˜… <p>In this episode, host Brian Okken and guest Adam Johnson explore essential Git features, highlighted by Adam's updated book, "Boost Your Git DX."Ā </p><p>Key topics includeĀ </p><ul><li>"cherry picking" for selective commits</li><li>"git stash" for managing in-progress work</li><li>"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</li><li>"git bisect" for efficiently pinpointing bugs.Ā </li></ul><p>This conversation offers valuable strategies for developers at any skill level to enhance their Git proficiency and optimize their coding workflows.</p><p><br>Links:</p><ul><li><a href="https://adamchainz.gumroad.com/l/bygdx">Boost Your Git DX</a> - Adam's book</li></ul> <br><p><strong>Help support the show AND learn pytest:Ā </strong></p><ul><li><a href="https://file+.vscode-resource.vscode-cdn.net/Users/brianokken/projects/test_and_code_notes/new_ad.md">The Complete pytest course</a> is now a bundle, with each part available separately.<ul><li><a href="https://courses.pythontest.com/pytest-primary-power">pytest Primary Power</a> teaches the super powers of pytest that you need to learn to use pytest effectively.</li><li><a href="https://courses.pythontest.com/using-pytest-with-projects">Using pytest with Projects</a> has lots of "when you need it" sections like debugging failed tests, mocking, testing strategy, and CI</li><li>Then <a href="https://courses.pythontest.com/pytest-booster-rockets">pytest Booster Rockets</a> can help with advanced parametrization and building plugins.</li></ul></li><li>Whether you need to get started with pytest today, or want to power up your pytest skills, <a href="https://courses.pythontest.com">PythonTest</a> has a course for you.<p></p></li></ul> <strong> <a href="https://www.patreon.com/c/testpodcast" rel="payment" title="ā˜… Support this podcast on Patreon ā˜…">ā˜… Support this podcast on Patreon ā˜…</a> </strong>

July 30, 2025 02:01 PM UTC


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 Walkthrough

Test 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:

  1. Asynchronous routines can pause their execution while waiting for a result and allow other routines to run in the meantime.
  2. 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:

Concurrency versus parallelismDiagram Comparing Concurrency and Parallelism in Python (Threading, Async I/O, Multiprocessing)

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 ]

July 30, 2025 02:00 PM UTC


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

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

(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

Incompatible changes, removals and new 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

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

July 30, 2025 01:29 PM UTC


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:

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:

Boomslang in 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:

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:

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:

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 @ondecorator, which allows you to bind the event to a specific widget or widgets.

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.tcssand 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 Horizontalcontainer’s height to auto, which tells Textual to make the container fit its contents. You also add a border to your OptionListand 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.pyand 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:

The next step is to subclass the Inputwidget 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 EditXMLScreenis 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 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_expandflag 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 Inputwidgets which are your special DataInputwidgets. 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:

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 Treewidget 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:

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.pyand 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:

That code is concise and straightforward.

Next, open up a text editor or use your IDE to create a file named add_node_screen.tcsswhich 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.pyand 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:

The last step is to apply a little CSS. Create a new file named preview_xml_screen.tcssand 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.pybut you are welcome to separate these two screens into different files. The first half of the file will contain the imports and the WarningScreenclass.

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 FileBrowserclass:

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 FileBrowserclass is more complicated because it does a lot more than the warning screen does. Here’s a listing of the methods:

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.pyand 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:

The last item you need to write is the CSS file for this dialog. You will need to name the file save_file_dialog.tcssand 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:

BoomslangXML TUI Demo

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.

July 30, 2025 12:30 PM UTC


Quansight Labs Blog

Learning from accessibility work

Years of accessibility work around Jupyter and thoughts on how to survive it in your own projects.

July 30, 2025 12:00 AM UTC


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:

  1. I only automate things that I do regularly.
  2. 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:

So if I’m using fewer slash commands, what am I doing instead?

  1. 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.
  2. 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.

July 30, 2025 12:00 AM UTC