skip to navigation
skip to content

Planet Python

Last update: August 15, 2026 09:48 PM UTC

August 15, 2026


Juri Pakaste

TIL: Typed do in Swift

TL;DR: Swift has do throws(MyError). It's helpful.

This is one of those "I can't believe I had missed that" things.

I've been pretty enthusiastic about adopting typed throws in Swift. If you need to process certain kinds of errors, it just makes sense to me. However, it was always a bit painful. You have this:

do {
    // throwing code here
} catch {
    // error is precisely typed here, assuming
    // the block above throws just one type
}

… and all is fine and good, except that the "throwing code" doesn't need to grow particularly complex before Swift decides to widen the type to any Error and your catch block starts producing compilation errors.

When I ran into this, I'd use catch let error as MyError. Fine. Except it isn't, because now your error-handling code is incomplete: you have to add an unreachable catch-all block, and you're unhappy with the language and your life choices.

I did page through the enhancement proposal when it passed through Swift Evolution, but I just never noticed that it doesn't end there. What you do in this situation is this:

do throws(MyError) {
    // throwing code here
} catch {
    // now the compiler doesn't get confused and
    // all is well-typed unicorns and sunshine
}

Now you know too.

August 15, 2026 11:30 AM UTC

August 13, 2026


PyCharm

Open weight models are having a moment, driven by control, choice, and cost. Hybrid and local AI are now getting serious looks, so JetBrains teamed up with DeepLearning.AI on a free AI Coding Workflows: Hybrid to Local course that covers the ideas and options. The course is now available and uses PyCharm and its AI […]

August 13, 2026 01:41 PM UTC


Python Software Foundation

Announcing the Packaging Council Election Candidates for 2026!

August 13, 2026 10:13 AM UTC


PyCharm

Hybrid and Local AI course at DeepLearning.AI

August 13, 2026 10:01 AM UTC


Python Software Foundation

Announcing the PSF Board Candidates for 2026!

August 13, 2026 09:59 AM UTC


Wingware

Wing Python IDE Version 12.0.2 - August 13, 2026

Wing Python IDE version 12.0.2 has been released. This release adds a native ARM64 Windows version of Wing, improves remote development on Windows, streamlines Claude Code setup, and improves performance and responsiveness, particularly when working with very large projects and on Windows. It also reduces the size of the analysis cache database by about 20% and fixes a number of bugs. See the change log for details.

Wing 12 integrates the Claude Code AI coding agent directly into the IDE, with a new Claude Code tool, a Tasks tool for planning and reviewing AI agent work, and a set of MCP servers that give the agent access to Wing's source code analysis, unit testing, debugger, and code review features. See the list of Wing 12 features below for details.

Wing 12 Screen Shot

Downloads

After installing Wing 12, be sure to Check for Updates in Wing's Help menu so that you have the latest hot fixes.

Wing 12 -- the full Python IDE, available as Wing Pro (for agentic development) or Wing Classic (for manual development) depending on your license, with a free 30-day trial of Wing Pro.

Wing 101 v. 12 -- a simplified free Python IDE for teaching beginning programmers.

Wing 11 and earlier versions are not affected by installation of Wing 12 and may be installed and used independently. However, project files for Wing 11 and earlier are converted when opened by Wing 12 and should be saved under a new name, since Wing 12 projects cannot be opened by older versions of Wing.

New in Wing 12

AI Coding Agent Integration with Claude Code

Wing 12 adds a Claude Code tool that integrates the Claude Code AI coding agent with the IDE. Set Up for Claude Code in the Project menu configures the active project for AI agent development.

A set of MCP (Model Context Protocol) servers gives Claude Code access to Wing's source code analysis, testing, and debugger functionality, so the agent can more efficiently navigate and understand your code, write, run, and fix unit tests, and use the debugger to diagnose difficult runtime errors. In our benchmarks, giving Claude Code access to Wing's MCP servers made agent-driven coding tasks both faster and cheaper.

Tasks Tool

The new Tasks tool lets you plan, queue, execute, review, and audit the history of AI agent development tasks, making it easier to supervise and inspect the agent's work before committing it to revision control.

FIX Features and Write Tests

Wing 12 adds AI agent driven FIX features that hand the current debugger bug, failing unit tests, or code warnings to Claude Code for resolution. New Write Tests items in the Testing and editor context menus prompt the agent to write unit tests for selected code.

Code Actions

Wing 12 also adds AI Code Actions, accessed from the FIX icon in the editor toolbar, that operate on selected code or the enclosing scope. Built-in actions include explaining code, reviewing it for quality or security risks, fixing code warnings, optimizing for performance, and updating comments and docstrings. The action list is user-extensible, so you can add your own prompts for tasks you run often.

Pseudo-Terminal for OS Commands and Debug I/O

The OS Commands and Debug I/O tools now default to using a pseudo-terminal that implements full ANSI terminal emulation, so you can run and debug programs that use color output, cursor positioning, or full-screen TUIs.

Redesigned OS Commands Capability

The OS Commands tool has been replaced with configurable OS Commands in the Tools menu. Each OS Command acts like its own tool, for use in any tool or editor split.

Tools in Editor Splits and Reorganized Tools Menu

Tools can now also be added or dragged to editor splits, allowing for much more flexible workspace layout. The Tools menu has been reorganized into related groups, with less-used and legacy tools in an Other sub-menu, so more commonly used tools are easier to find.

Test Discovery and Preferences Search

Wing 12 adds automatic test file discovery and discovery of individual unit tests within files, so you usually don't need to specify test file patterns or add test files individually. The Preferences dialog now supports text search and back/forward navigation.

Other Minor Features and Improvements

Wing 12 also includes many other improvements, including:

  • IDE build for ARM64 Windows
  • Improved performance and responsiveness
  • Improved remote agent installation and remote development
  • Significantly faster source code analysis
  • Prompts for SSH passphrases and HTTPS credentials when needed during VCS operations
  • Faster detection of externally modified files, with reduced CPU load
  • Saving and restoring of tool console scrollback across project close and reopen
  • Clickable OSC 8 hyperlinks in the OS Commands and Debug I/O tools
  • A preference to select the ssh or plink.exe SSH implementation
  • A notice on the next startup when Wing's previous session ended in an unexpected crash

Wing 12 also makes a number of other bug fixes and usability improvements.

Product Line Changes

Wing 12 simplifies the product line. The Commercial / Non-Commercial use distinction has been replaced by two feature-based product tiers:

  • Wing Pro -- the full-featured Python IDE including AI agent development tools
  • Wing Classic -- the complete traditional Python IDE for hands-on development, with no AI agent features

Anyone may purchase either tier for any purpose. Existing Commercial and Non-Commercial Use licenses both become Wing Pro. Customers who don't need the AI agent features may move to Wing Classic at renewal time, or any time sooner by contacting support@wingware.com.

Wing Personal has been discontinued. Existing Wing Personal users may continue to use Personal 11.x indefinitely, switch to free Wing 101, or purchase a Wing Classic license. See Pricing for details.

Changes and Incompatibilities

The single-LLM-query AI features originally introduced in Wing 11 (the AI Coder and AI Chat tools) are considered legacy in Wing 12 and hidden from the user interface by default. They remain available in projects that already use them and can be re-enabled with Project Properties > AI in Project Properties or in the Projects > AI preferences.

See Wing's Claude Code Agent Integration for Wing 12's AI agent approach.

If you have questions, please don't hesitate to contact us at support@wingware.com.

August 13, 2026 01:00 AM UTC


Trey Hunner

Reorganizing Python's sys module

August 13, 2026 12:30 AM UTC


Python Insider

Announcing the Packaging Council Election Candidates for 2026!

Announcing the 17 nominees for the inaugural Python Packaging Council election, and how to vote in the election.

August 13, 2026 12:00 AM UTC

August 12, 2026


Django Weblog

DSF Office Hours

The DSF Board hosts open office hours every Wednesday at 6:00 PM UTC (check your local time). Anyone in the Django community is welcome to drop in. You do not need an agenda or an invitation. Video call details are on the DSF Office Hours page.

We have been running these since October 2024, and right now we have two things we would especially like to talk with you about.

Who shows up

On any given Wednesday, you might find DSF Board members, Steering Council members, Django Fellows, working group members, and community members who are curious about joining a working group. There is no membership requirement. Anybody from the community can join, and often does.

We recently published a call for applicants for a Django Executive Director. If you are considering applying, or you are still deciding whether it is the right fit, come to office hours and ask us anything: what the job actually looks like, what we expect in the first year, how the search works. We would rather answer your questions directly than have you guess from a job posting.

If this is the first you are hearing about the search, please help us spread the word. The best candidate may be someone who has not thought to look.

Fundraising to support it

Hiring an Executive Director is why we raised our 2026 fundraising goal from $300,000 to $500,000, which needs about $16,000 per month in additional recurring support. We have made real progress and are working to close the rest.

If your company uses Django, you can help through corporate sponsorship, a direct donation, or GitHub Sponsors. Most of these are a small lift for a company that already depends on Django. If you want fundraising materials to bring to your leadership team, or help picking the option that fits, come to office hours, and we will get you what you need.

Everything else

Office hours cover plenty beyond that: what our working groups are up to and how to join one, projects the Foundation is working on, and whatever you have been wondering about how the DSF operates. In the weeks before a board meeting, we use the time to gather feedback on what we are about to discuss. If you want the board to hear something, this is a direct line.

One thing office hours are not: a general Django support channel. It is not the place to debug your code or market a product. For coding help, the Django Forum will get you faster answers.

Office hours are the most direct way to keep up with the Foundation, but they are not the only one. We wrote up all the other places we post and where the conversation happens if Wednesdays do not work for you.

Otherwise, put a Wednesday on your calendar and say hello.

August 12, 2026 05:25 PM UTC


PyCharm

What’s New in PyCharm 2026.2.1

This PyCharm release is a big one for anyone building with AI. Your agents can now roll up their sleeves inside your Jupyter notebooks – working against a live kernel instead of firing off disconnected scripts. And they finally know which Python to use, so packages land in the right environment every time. We’re also […]

August 12, 2026 12:05 PM UTC

We Stopped AI Agents From Installing Into the Wrong Python: Task Success Rates Jumped to 95%+

AI agents are supposed to save you time. Ask one to install a dependency or run your project, though, and it often does the opposite: It installs into the wrong Python, ignores the uv or virtual environment your project uses, and hands back a broken setup for you to fix yourself. PyCharm’s new Agent Environment […]

August 12, 2026 12:01 PM UTC

We Gave AI Agents a Live Jupyter Kernel in PyCharm

If you’ve handed notebook work to an AI agent, you know how it tends to go: More often than not, it corrupts your .ipynb, loses your trained model the moment the run finishes, or burns budget sitting idle through a long job while you watch. To solve this, we’re introducing a brand-new Jupyter skill. Built […]

August 12, 2026 12:00 PM UTC

Unbundling and Deprecating Low-Usage Plugins in PyCharm

As part of ongoing maintenance, we are unbundling and deprecating low-usage plugins starting with PyCharm 2026.2. This includes support for Data Wrangler, Hugging Face, and Google Colab, among others.  A more focused set of bundled plugins means a leaner codebase, enabling us to keep PyCharm fast and responsive and invest our effort where it has […]

August 12, 2026 11:59 AM UTC


Python GUIs

Adding QTabWidget to a Layout Alongside Other Widgets in PyQt6 — How to combine QTabWidget with other layouts without it taking over your entire window

I'm trying to create a layout in PyQt and implement different layouts into one QHBoxLayout. Everything works fine, but when I add a QTabWidget alongside other layouts, it becomes the only visible widget — as if everything else disappears. Isn't it possible to have a QTabWidget in a layout beside other layouts?

August 12, 2026 06:00 AM UTC


Python Bytes

#491 Feeling Judged

Topics include , Post-quantum crypto lands in Python, MCP goes stateless — and FastMCP gets renamed, and inshellisense - IDE style command line auto complete.

August 12, 2026 01:00 AM UTC


Python Insider

Python 3.12.14, 3.11.16 and 3.10.21 are now available!

[Python Releases Eclipse Your Security!](https://discuss.python.org/t/python-3-12-14-3-11-16-and-3-10-21-are-now-available/108537) New security releases for 3.10, 3.11 and 3.12 are now available.

August 12, 2026 12:00 AM UTC

August 11, 2026


TestDriven.io

Storing Django Static and Media Files on Cloudflare R2

This tutorial shows how to configure Django to load and serve up static and media files, public and private, via Cloudflare R2.

August 11, 2026 10:28 PM UTC


PyCoder’s Weekly

Issue #747: Modern OOP, bisect, Django Async, and More (2026-08-11)

August 11, 2026 07:30 PM UTC

August 10, 2026


Brett Cannon

My nomination statement for the 2026 Python packaging council

I have decided to run for the inaugural/2026 Python packaging council (PPC). I will say I have the support of my employer (Microsoft) to do this, but they didn&apost ask me to and my usual thing that I would quit before I let any employer pressure me

August 10, 2026 10:22 PM UTC


James Bennett

Breaking up (lines) is hard to do

Here’s a seemingly simple question: given a chunk of multi-line text, how do you split it and return an array whose members are the constituent lines of the text?

Hopefully, your first instinct is to reach for some sort of standard-library function, maybe something like the splitlines() method of Python’s str type. Because it turns out this “simple” question is actually pretty complex to answer! For example, quite some time ago I read a post by William Woodruff pointing out the surprising discovery that Python treats up to eleven different Unicode code points or code point sequences as indicating a line break.

At the time I meant to write about that, but a lot of other things started fighting for my time, and it’s only now that I’m finally digging it out of my drafts. Still, better late than never, so today let’s dig into some of the many ways there are to break a line of text and how they’ve been standardized and specified and ultimately wound up in the set Python uses.

In the beginning…

Once upon a time, there was ASCII. Of course there were other things before ASCII, and alongside ASCII, but for today’s discussion we really only need to go back to ASCII; if you want the full history of physical teletypes, how they evolved from typewriters and influenced character sets for computing and so on, I suggest Wikipedia. Here, I’m just going to gloss over and simplify a lot of that to focus on the topic at hand.

So. Once upon a time, there was ASCII. And it wound up being incredibly influential and important in computing, to an extent other early character sets couldn’t match. And because it was used on computers which used teletypes (basically electronic typewriters connected as input/output devices) as a user interface, it contained control characters for sending commands to the teletype. Such as a LINE FEED (byte value 0x0A) to advance the paper vertically to the next line, and a CARRIAGE RETURN (byte value 0x0D) to re-align the print head/carriage with the horizontal start point of the line.

These are often abbreviated LF and CR (or by their C-family escape sequences \n and \r, respectively), and you might think that since physically advancing a typewriter-style device to be ready to print the next line requires both operations, that would have just become the universal way everybody did new lines. Or at least the universal way everybody did them in English, or in the US, where ASCII dominated. Right?

Well, nothing is ever that simple. Physical teletypes apparently benefited from the two-character approach (as opposed to a single “new line” character) because it gave them time to physically move everything into the right position. But as virtual teletypes—“printing” to a television-like display instead of to paper—became more common, that was less of an issue. So there were multiple possible options for representing line breaks, and several of them showed up in historical systems. For example:

This meant “plain text” was not easily portable between these various systems, since none of them could agree on how to represent a line break. Which led to one of my all-time favorite programming jokes, in the infamous NOT the comp.text.sgml FAQ document:

Q. What’s an RE?

A. RE is an acronym for Record End, which is sort of like a newline, only different. Goldfarb’s First Law of Text Processing states that:

… if a text processing system has bugs, at least one of them will have to do with the handling of input line endings.”

[The Handbook, footnote p. 321]

The Record End concept was introduced to make sure that SGML parsers don’t violate Goldfarb’s First Law.

(for the uninitiated, Charles Goldfarb created SGML)

Anyway, over twenty years ago Python tried (in Python 2.3) to smooth this over by introducing “universal newline” mode for opening files, which accepts all three options: a plain \n (Unix), or a plain \r (classic Mac), or an \r\n sequence (DOS and Windows) will all be interpreted as line breaks.

But even in ASCII there there are other ways of breaking a line. For example, at byte value 0x0C ASCII includes the FORM FEED control character (FF, or \f). Which is not one of the traditional characters used by major operating systems as a “newline”, but nonetheless does cause a new line to occur: it moves to the next page (if necessary, by ejecting the current sheet of paper from the printer and feeding in a new one). And there’s also 0x0B, VERTICAL TAB (VT or \v): just as a “regular” tab (\t) causes a horizontal adjustment, a vertical tab causes a vertical one. So it, too, causes output to advance to another line (probably skipping several in the process).

And the C1 control characters added 0x85, the NEXT LINE character (typically abbreviated NEL), useful for translating back and forth between ASCII and IBM’s EBCDIC character set (which had “New Line” as a single character).

Then Unicode happened

Today we live in a Unicode world, and Unicode tries its hardest to catalog and standardize and describe how to work with all the world’s writing systems. Chapter 5, Section 8 of the Unicode Standard, “Newline Guidelines”, lists seven code points to recognize as causing new lines. Five of them we’ve seen already:

The CR LF sequence is also recognized, on systems which use it.

But the other two code points are new and were created specifically for Unicode:

The Unicode Standard explains that the traditional newline characters had started to become ambiguous, because of the rise of tools such as word-processing programs which implicitly broke lines to wrap them for display and so began using explicit “newline” characters to mean a paragraph break rather than a line break. So Unicode added two new code points whose purposes are explicit. And the standard says that “[I]n Unicode text, the PS and LS characters should be used wherever the desired function is unambiguous.”

This set of line-breaking code points originated in version 5.0 of Unicode, with Unicode Technical Report #13, which lists the seven “newline” code points and the CR LF sequence. This is also the set of code points and sequences defined for line boundaries in Unicode regular expressions, Unicode Technical Standard #18.

And expanding on Chapter 5 of the Standard, there’s Unicode Standard Annex #14, “Unicode Line Breaking Algorithm”. As the name implies, this document formally specifies the line-breaking algorithm for Unicode, including defining things like which characters offer an opportunity to break a line, whether the break is mandatory, and whether the break would come before or after the character in question. It does this in a typical Unicode way: by defining a set of named properties and specifying which characters have which properties.

Two ways about it

But there are still three “newline” characters supported by Python that we haven’t seen yet, and they come from a place that might be surprising: Unicode Standard Annex #9, the bidirectional algorithm. And it’s OK if you’re wondering what that has to do with newlines, because it’s not immediately obvious if you don’t already know about it.

Some written scripts, like the Latin script this blog post is written in, are written and read left-to-right: the start of a line of text is on the left-hand side, and the end is on the right-hand side. Other scripts, such as Arabic or Hebrew, do the opposite, and are right-to-left. And so Unicode, which again wants to cover all the world’s writing systems and let you use any or all of them, has to support both left-to-right and right-to-left horizontal text direction.

But more than that, it has to support switching direction within a single piece of text. You might have something that’s in, say, Arabic but quotes something in Spanish in the middle of a line; that would require a short section of left-to-right inside an otherwise right-to-left text. Or you might be writing something that uses boustrophedon, switching directions on each line. So Unicode includes direction-control characters like U+200E LEFT-TO-RIGHT MARK and U+200F RIGHT-TO-LEFT MARK to handle this. But it also needs to know the scope of a direction change, and that’s where the last “newline” characters come in: the Unicode bidirectional algorithm says that “[t]he effects of all of these formatting characters are limited to the current paragraph; thus, they are terminated by a paragraph separator”.

So Unicode characters have, among their properties, a “bidirectional class” which influences how they affect the bidirectional algorithm. And the characters which act as paragraph separators for purposes of ending the effects of an explicit directional marker all share a common value for this: bidirectional class B. The characters with that class include quite a few that we’ve already seen, along with three more characters:

But these are better known by their original ASCII names: FILE SEPARATOR, GROUP SEPARATOR, and RECORD SEPARATOR. ASCII provided these to help represent data structures in memory and on storage media. Today it’s not as common to try to use control characters for this purpose, though they do have the virtue of being rare in actual text, unlike other common delimiters such as tab or comma.

End of the line

And now, after looking at multiple character sets and five Unicode technical documents, we can finally state clearly what’s going on in Python.

Python’s splitlines() treats ten different code points, and one multi-code-point sequence, as causing a line break. These are:

Which is also exactly what’s stated by a comment in the CPython source code accompanying the list of individual code points that are considered to break lines, but hopefully now you have a better understanding of what that comment means and how this particular set was arrived at.

August 10, 2026 04:42 PM UTC


Talk Python to Me

#558: Hyper-Personal Software with Python

Every company has one. The little internal tool that Jane built back in 2021, and then Jane left. Nobody understands it, nobody will touch it. There are two unwritten rules around it: don't change it, it's working. And if you break it, you bought it. That's dark-matter enterprise software. For every app you can actually see, there are ten of these sitting in the shadows, frozen. Michael Booth thinks that just changed. He read my article on hyper-personal software and ran with it, writing about hyper-team software: small teams inside big companies finally building the tools that were never going to get built. We cover where this works, where it quietly goes wrong, and the guardrails that keep it from turning into a mess. Let's get into it.

August 10, 2026 01:21 PM UTC


Ed Crewe

From Routing Checks to Trajectory Testing: Evaluating an Agentic Chatbot

August 10, 2026 10:37 AM UTC


Django Weblog

Django is moving to an annual release cycle

Django's Steering Council has accepted the Django Enhancement Proposal DEP 20 to move Django to an annual release cycle. From January 2028, Django will make one feature release a year, giving every feature release the LTS-level three years of support, and version numbers will carry the feature release year: Django 2028, then Django 2029, and so on.

Better Python support

Python releases annually, each October. Django's eight-month cycle hasn't fit that well: LTS releases carried a wide Python matrix, including versions long past their upstream end-of-life.

Under the new cycle, each Django version supports the three latest Python versions at release, and picks up the new Python version during its first year. Django's support window ends in step with its oldest supported Python.

Every release is an LTS

Every feature release gets three years of support: one year of mainstream bugfixes, then two years of security and data-loss fixes. The "LTS" label is retired — every feature release now carries that same, unique commitment.

No more LTS gap: no racing a deadline to jump two years of changes at once. Upgrade one year at a time, whenever suits you within the support window. Three versions are supported at any time, giving third-party packages a clear, rolling target.

API stability and deprecation policies are unchanged — deprecation periods actually get longer in calendar terms.

Transition timeline

Django 2028 will be the first release under the new cycle.

Release Date End of mainstream support End of extended support
Django 6.1 August 2026 April 2027 December 2027
Django 6.2 LTS April 2027 December 2027 April 2030
Django 2028 January 2028 January 2029 January 2031
Django 2029 January 2029 January 2030 January 2032
Django 2030 January 2030 January 2031 January 2033

Nothing changes before 2028. Support commitments for Django 5.2 LTS and 6.2 LTS stand as made.

Read the DEP

DEP 20 has the full specification and the reasoning behind each decision. Thanks to everyone who took part in the discussion, and to the Steering Council for its consideration.

August 10, 2026 09:00 AM UTC

August 09, 2026


Ned Batchelder

Caller-specific coverage

I’ve had an idea rattling around to get more detail from coverage measurement. Can we measure the coverage in a function separately for each caller of the function?

Here’s why I want it: in Acidica, my toy BASIC interpreter, I had code to implement the built-in functions that looked something like this:

match func_name:


    case "LEN":
        if len(args) != 1:
            raise TypeError(f"Wrong arguments for LEN, got {len(args)}")
        return len(args[0])

    case "LEFT$":
        if len(args) != 2:
            raise TypeError(f"Wrong arguments for LEFT$, got {len(args)}")
        return args[0][:args[1]]

    # ... 19 other built-ins ...

I didn’t like the repeated code here: each different func_name has to check that it got its expected number of arguments and perhaps raise an error. So I refactored:

def expects(nargs: int, func_name: str, args: tuple) -> None:

    if len(args) != nargs:
        raise TypeError(f"Wrong arguments for {func_name}, got {len(args)}")

match func_name:
    case "LEN":
        expects(1, func_name, args)
        return len(args[0])

    case "LEFT$":
        expects(2, func_name, args)
        return args[0][:args[1]]

Nice. The code is tighter, easier to read, and common behavior is implemented in one place.

But the old code had an advantage: because each error condition had its own raise line, coverage measurement could tell me whether I had tested every func_name for the wrong number of arguments. With the error handling happening in a helper function, that information is lost. I’ll know that some func_name had a test for the wrong number of arguments, but not that all of them did.

Here’s where the new idea comes in. What if I could indicate that for the expects function, I want separate coverage data for each distinct calling site? Then I could see that every func_name had a test for both the wrong number of arguments and the right number of arguments. The simple branch inside expects would be measured separately for each caller.

I have a quick proof-of-concept. A decorator on expects does the work. Coverage.py already has dynamic contexts which are used for things like tracking which tests called which code. The decorator starts a new context named for the calling location, then restores the context when the function returns:

def coverage_per_caller(func):

    @functools.wraps(func)
    def _wrapper(*args, **kwargs):
        cov = coverage.Coverage.current()
        name = func.__name__
        caller = inspect.currentframe().f_back
        file = caller.f_code.co_filename
        lineno = caller.f_lineno
        prev_context = cov.switch_context(f"per_caller:{name}:{file}:{lineno}")
        try:
            ret = func(*args, **kwargs)
        finally:
            cov.switch_context(prev_context)
        return ret

    return _wrapper

I had to make one tiny (unreleased) change to coverage.py for this: switch_context used to return None, but now it returns the previous context so that we can nest them properly.

To my delight, this works! I can look at the HTML coverage report and see the caller contexts for the lines in expects. I can see that 20 callers ran the if line, but only 2 ran the raise, and the context names show the file and line number of the callers for each:

HTML report showing the contexts that ran each line of expects()

This isn’t the whole solution yet. Things to improve:

But it’s a start, and gives me other ideas. I could use some aspect of the data passed into a function as the context name. In this example, we could have used func_name as the context instead of the caller’s location. Maybe you have ideas for other uses.

August 09, 2026 05:56 PM UTC


LernerPython blog, from Reuven Lerner

Free real-world Pandas exercises, with solutions

If you want to get better at Pandas, the hard part isn’t finding tutorials. It’s finding problems worth solving. Most exercises hand you a tidy little table of five rows […]

The post Free real-world Pandas exercises, with solutions appeared first on LernerPython.

August 09, 2026 02:12 PM UTC