skip to navigation
skip to content

Planet Python

Last update: November 03, 2026 04:48 PM UTC

November 03, 2026


Wing Tips

Getting Started with Autonomous Development in Wing Pro 12.1

This Wing Tip describes how to get started with autonomous development in Wing Pro 12.1. You write a specification for a new application or feature, and Wing's Tasks Manager fleshes it out, splits it into tasks, and runs them with a team of AI agents that write the code, unit tests, and documentation. You answer questions and review the results in one place, the Tasks tool.

This Wing Tip is part of a series of Wing Tips on working with AI agents in Wing. For more detail on each step, see the How-To Autonomous Development with Wing Pro.

Getting Started

We recommend using a reasonably isolated environment, such as a virtual machine, when using AI agents. At the very least, prevent agents from accessing the keys to your server infrastructure, reading sensitive data, or pushing to your main repository.

(1) Install an AI agent and set up an account for it that covers the amount of work you expect to do. For sustained use, a subscription is generally much cheaper than paying for usage on an API key. Claude Code and Codex are best for autonomous development, since they check the safety of the commands they run. Antigravity, Copilot, and Cursor also work but don't provide the same level of safety. See Setting Up an AI Agent and AI Agent Safety for details.

(2) Install git or Mercurial, if you don't already have one of them. Wing uses it to track, review, and commit each task's changes. You don't need to learn how to use it, since Wing and the agents do that for you.

(3) Create a new project with New Project in the Project menu. Choose New directory and enter a Name, then choose your agent under AI Agent and a Python environment under Python. Leave Create a Git repository checked and click Create Project. Setting up the project for an agent turns on the Tasks Manager and lets it commit autonomously:

Setting Up an AI Agent in the New Project Dialog

Shown above: Setting up a new project for an AI agent.

(4) Check the Tasks Manager's settings with the settings icon at the right of the Manager's bar, at the top of the Execute tab in the Tasks tool. The defaults are a good place to start. We recommend leaving automatic commits on, so work can go on while you are away. You can still look over every committed task on the Review tab and ask for changes.

The Tasks Manager settings dialog

Shown above: The Tasks Manager's settings.

(5) Write a specification or tasks on the Plan tab of the Tasks tool, and mark them Ready by clicking their Draft buttons. A task can be anything from a bug report to a high-level specification for a whole application. Describe what the code should do and any technical constraints that matter from the start. The Manager researches the rest, asks you questions, and writes out the tasks:

The Plan tab with Ready tasks

Shown above: Tasks on the Plan tab, marked Ready.

You can watch progress on the Execute tab, and click a queue's name to see its session in the AI Agent tool. Problems that agents notice along the way, and follow-up tasks the Manager proposes, appear in the Incoming view on the Plan tab for you to make Ready or dismiss.

(6) Answer questions on the Questions tab when the Manager or a task needs your input. Other tasks keep running while it waits. You can also talk to the Manager directly on the Execute tab, to ask about progress or give it instructions such as "ask me first about anything that changes a file format."

The Questions tab with an open question

Shown above: A question from a worker, waiting on the Questions tab.

(7) Review finished work on the Review tab. Each task reports a summary of its changes and the decisions made in it. Use Revise to ask for changes to any decision or the task as a whole. If you are committing work yourself, use Start Review to view and comment on the code, or Commit to commit it without further review.

The Review tab with a task report and the Start Review and Commit links

Shown above: A finished task's report on the Review tab, with automatic commits turned off.

Usage Tips

For better results, build tests and cross-checks into your process:

  • Ask the Manager early on to set up a way for agents to run your application as a user would, so they can see the results of their work.
  • Add tasks at key points that review the code for security flaws, gaps in test coverage, or anything else that matters to you. Problems they find automatically become new tasks.
  • Let the Manager commit on a separate branch, and review that branch before merging it into your main branch.
  • Use the application yourself, and add tasks for the bugs and improvements you find.

If you're unfamiliar with any of this, just tell the Manager what you need, in the Execute tab of the Tasks tool. It can help you set up your development environment and get started with autonomous software development.

Further Reading

See The Tasks Manager Agent in the Wing Pro manual for details. For examples of complete projects built this way, see the Autonomous Development Demos.



That's it for now! We'll be back soon with more Wing Tips for Wing Pro.

As always, please don't hesitate to email support@wingware.com if you run into problems or have any questions.

November 03, 2026 04:48 PM UTC


death and gravity

pyinfra – simple infrastructure automation in Python

Hi, I'm Adrian, and today I will live demo pyinfra, an infrastructure automation tool.

We will deploy two websites – a landing page and a Python web app – first on a local virtual machine, and then on two remote servers, using only Python code.

Note

This is the edited transcript of a talk I gave at a local DevOps meetup. While part of the "live" aspect is lost in writing, I think it's a pretty decent pyinfra tutorial, so you are welcome to follow along at home.

Contents

About me #

Before we talk about pyinfra, I should introduce myself.

I worked at a Fortune 10 company for most of my career, where I did site reliability engineering for the reverse proxy behind some really big websites and two thousand smaller ones, and where I built a data-lake-plus-feature-store for an ML organization.

In my spare time, I write about Python on this website, and I maintain the creatively named reader, a Python feed reader library and web app. Lately I've been working on a hosted version of reader, which is how I ended up using pyinfra.

What is pyinfra? #

pyinfra is a mostly command-line tool that turns Python code into shell commands and runs them on many servers over SSH, in parallel. Think Ansible, but simpler.

deploy.py
for user in ['admin', 'alice', 'bob']:
    server.user(user)

files.put('files/hello.txt', '/etc/motd')

if host.data.get('install_postgres'):
    apt.install('postgresql')

Why use pyinfra? #

I chose pyinfra because, like Ansible, it is declarative – you describe what a server should look like, and pyinfra makes it so – and idempotent – nothing happens if the server is already in the desired state.

Unlike Ansible, there's no YAML – deploys (playbooks) are Python code, which means you can do anything Python code can do: you can have fors and ifs, you can have code completion and type checking, and you can even import libraries.

Of interest to people using Ansible at larger scales, pyinfra is also faster; in my limited, small-scale experience, it was faster than Ansible with the Mitogen plugin.

In fairness to Ansible, while pyinfra has been around for 10 years, it has fewer built-in operations (modules) and a smaller ecosystem, although that's partially offset by how easy it is to write custom operations.

When to use pyinfra? #

pyinfra use cases follow from what it does – configuring servers over SSH.

As such, pyinfra is good for boring, cloud-agnostic architectures, from lots of servers with different roles all the way down one-computer programming, where it still makes deploys reproducible and prevents drift.1

It is pretty easy to learn too, so it's a good fit for self-hosting and home servers, and even if you don't use it for configuration management, pyinfra gives you a convenient way of running ad-hoc commands on fleets of servers.2

The simplicity comes with limitations: fully-automated pipelines are extra work, and care is needed if multiple people run deploys at the same time. Nevertheless, you can get pretty far if you structure your deploys well, and there's a Python API for building stuff on top.

The demo #

I'll try to focus on the interesting parts, but you can find the entire code on GitHub.

At the end of the demo, we'll have two websites, a landing page and a Python web app, deployed in two stages – a local virtual machine for beta:

.--- pyinfra.local ---.- app.pyinfra.local -.
|                   nginx                   |
|   static website    :   Python web app    |
'-------------------------------------------'

...and two separate servers for prod:

.--- pyinfra.a.c ----. .- app.pyinfra.a.c --.
|       nginx        | |       nginx        |
|   static website   | |   Python web app   |
'--------------------' '--------------------'
Here's a sneak peek of the final deploy:
infra/deploy.py
apt.update(name="apt update", cache_time=24 * 3600)

apt.packages(['nginx', 'python3-certbot-nginx'], name="install nginx")

disable_default = files.link(
    name="disable default site",
    path='/etc/nginx/sites-enabled/default',
    present=False,
)
server.service(
    name="reload nginx",
    service='nginx',
    reloaded=True,
    _if=disable_default.did_change,
)

if host.data.get('home_domain'):
    local.include('tasks/home.py')
if host.data.get('app_domain'):
    local.include('tasks/app.py')

Operations and facts #

First, we should get familiar with two basic pyinfra concepts.


Operations change the server in some way; most are declarative and idempotent, and you use them to ensure state. A Python file containing operations is called a deploy.

Here's a simple deploy with one operation that says "I want hello.txt to exist":

examples/create_file.py
from pyinfra.operations import files

files.file('hello.txt', present=True)

If we run this deploy (below, on my @local machine), we see a list of all the operations and on which hosts they will produce changes, and are asked to approve them:

$ pyinfra @local examples/create_file.py
--> Loading config...
--> Loading inventory...
--> Connecting to hosts...
    [@local] Connected

--> Preparing operation files...
    Loading: examples/create_file.py
    [@local] Ready: examples/create_file.py

--> Detected changes:
    Operation                              Change       Conditional Change
    files.file (hello.txt, present=True)   1 (@local)   -

    ...
    Press enter to execute...

If we do, we see the operations running and their outcomes:

    Press enter to execute... [Enter]
--> Beginning operation run...
--> Starting operation: files.file (hello.txt, present=True)
    [@local] Success

--> Results:
    Operation                              Hosts   Success   Error   No Change
    files.file (hello.txt, present=True)   1       1         -       -
    Grand total                            1       1         -       -

--> Disconnecting from hosts...

And now hello.txt exists:

$ ls hello.txt
hello.txt

Since files.file is idempotent, nothing happens if we run the deploy again:

$ pyinfra @local examples/create_file.py
...
--> Detected changes:
    Operation                              Change   Conditional Change
    files.file (hello.txt, present=True)   -        -
...
--> Beginning operation run...
--> Starting operation: files.file (hello.txt, present=True)
    [@local] No changes
...

Facts gather data about the target host; you use them to control operations.

Here's a deploy that installs the Apache web server:

examples/install_apache.py
from pyinfra import host
from pyinfra.facts.server import LinuxName
from pyinfra.operations import server

if host.get_fact(LinuxName) in ['Ubuntu', 'Debian']:
    server.packages('apache2')
else:
    server.packages('httpd')

The package is usually called httpd, but Debian-based distributions call it apache2, so we use the LinuxName fact to detect where we are and pass the right name to the server.packages operation. My machine is a Mac, so the httpd branch is chosen:3

$ pyinfra @local examples/install_apache.py --dry
...
--> Detected changes:
    Operation                 Change       Conditional Change
    server.packages (httpd)   1 (@local)   -
...

Where are the commands? #

Before we look at debug output, let's see how pyinfra operations are implemented.

Here's a simplified version of the files.file implementation:

@operation()
def file(path: str, present: bool = True):
    info = host.get_fact(File, path=path)

    if info is None and present:
        yield f"touch {quote(path)}"

    elif info and not present:
        yield f"rm -f {quote(path)}"

The structure is similar for all operations: read the host state using facts, diff it against the desired state, and return commands to reach the desired state if they don't match.


Here's another deploy that says "I want hello.txt to not exist":

examples/delete_file.py
from pyinfra.operations import files

files.file('hello.txt', present=False)

With --debug, we can see the commands being run during each execution phase.

During the prepare phase (before approval), pyinfra runs the deploy code once per host to load operations, and then runs each operation to see if it returns commands, without running the commands themselves (this is how "detected changes" works).

Running operations runs facts, which results in read-only commands being run:

$ pyinfra @local examples/delete_file.py --debug
...
--> Preparing operation files...
    Loading: examples/delete_file.py
    [pyinfra.api.operation] Adding operation, {'files.file'}, ...
    [pyinfra.api.facts] Getting fact: files.File (path=hello.txt)
[pyinfra.connectors.local] --> Running command on localhost: sh -c '! (test -e hello.txt || test -L hello.txt ) || ( stat -c '"'"'user=%U group=%G mode=%A atime=%X mtime=%Y ctime=%Z size=%s %N'"'"' hello.txt 2> /dev/null || stat -f '"'"'user=%Su group=%Sg mode=%Sp atime=%a mtime=%m ctime=%c size=%z %N%SY'"'"' hello.txt || ls -ld hello.txt )'
    [pyinfra.api.facts] [@local] Loaded fact files.File (path=hello.txt)
    [@local] Ready: examples/delete_file.py

--> Detected changes:
    Operation                               Change       Conditional Change
    files.file (hello.txt, present=False)   1 (@local)   -

    ...
    Press enter to execute...

During the execute phase, pyinfra runs the previously loaded operations again, but this time it runs the commands returned by them too:

    Press enter to execute... [Enter]
--> Beginning operation run...
--> Starting operation: files.file (hello.txt, present=False)
    [pyinfra.api.facts] Getting fact: files.File (path=hello.txt)
[pyinfra.connectors.local] --> Running command on localhost: sh -c '! (test -e hello.txt || test -L hello.txt ) || ( stat -c '"'"'user=%U group=%G mode=%A atime=%X mtime=%Y ctime=%Z size=%s %N'"'"' hello.txt 2> /dev/null || stat -f '"'"'user=%Su group=%Sg mode=%Sp atime=%a mtime=%m ctime=%c size=%z %N%SY'"'"' hello.txt || ls -ld hello.txt )'
    [pyinfra.api.facts] [@local] Loaded fact files.File (path=hello.txt)
[pyinfra.connectors.local] --> Running command on localhost: sh -c 'rm -f hello.txt'
    [@local] Success
...

Inventory + nginx setup #

So far we've been using my local machine as target host, but we want beta to be as similar to prod as possible, so I've set up an Ubuntu virtual machine with Vagrant.

Also, you will typically use an inventory file containing groups of hosts instead of passing individual hosts directly to the CLI. For our demo, we need just one group:

infra/inventories/beta.py
# group_name = [host1, host2, ...]

my_hosts = ['pyinfra.local']

# you can have more groups:
#
# db_servers = ['db-1.net', 'db-2.net']
# web_servers = [i.hostname for i in aws.ec2.list_instances(asg='my-asg')]

Since the inventory is a Python file, groups don't have to be list literals – you could, for example, import a library and list instances in an autoscaling group dynamically.

Here's how you use an inventory to run arbitrary commands:

$ pyinfra inventories/beta.py exec -- lsb_release -d
...
--> Beginning operation run...
--> Starting operation: server.shell (lsb_release -d)
[pyinfra.local] Description: Ubuntu 26.04.1 LTS
    [pyinfra.local] Success
...

We'll use nginx for both of our websites, which means we need to install it first.

Here's what the main deploy looks like (minus imports):

infra/deploy.py
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
apt.update(name="apt update", cache_time=24 * 3600)

apt.packages('nginx', name="install nginx")

disable_default = files.link(
    name="disable default site",
    path='/etc/nginx/sites-enabled/default',
    present=False,
)
server.service(
    name="reload nginx",
    service='nginx',
    reloaded=True,
    _if=disable_default.did_change,
)

We update the package cache, we install nginx, we disable the default "Welcome to nginx!" site, and then we reload the nginx config. To avoid unnecessary reloads, we use the _if global argument to reload only if the previous operation actually ran.

In theory, we could use a fact to gate the last two operations instead:

apt.packages(...)
if host.get_fact(Link, '/etc/nginx/sites-enabled/default'):
    files.link(...)
    server.service(...)

...but that if happens in the prepare phase, when nginx (and the symlink) hasn't been installed yet, so they would be skipped on the first run.

If we run the deploy, no change is detected for "disable default site" (no symlink yet):

$ pyinfra inventories/beta.py deploy.py
...
--> Detected changes:
    Operation              Change              Conditional Change
    apt update             1 (pyinfra.local)   -
    install nginx          1 (pyinfra.local)   -
    disable default site   -                   -
    reload nginx           -                   1 (pyinfra.local)

    Detected changes may not include every change pyinfra will execute.
    Hidden side effects of operations may alter behaviour of future operations,
    this will be shown in the results. The remote state will always be updated
    to reflect the state defined by the input operations.

    Press enter to execute... [Enter]
...
--> Results:
    Operation              Hosts   Success   Error   No Change
    apt update             1       1         -       -
    install nginx          1       1         -       -
    disable default site   1       1         -       -
    reload nginx           1       1         -       -
...

...but the operation does run, as the prophecy foretold ("side effects of operations may alter behaviour of future operations").

Liking this so far? Here's another article you might like:

Static site + code reuse #

With nginx installed, we can proceed to the static home page.

We'll put the operations in a separate file that we include in the main deploy, which in addition to keeping code tidy, allows specific tasks to be conditional:

infra/deploy.py
23
24
if host.data.get('home_domain'):
    local.include('tasks/home.py')

Host data is arbitrary data associated with hosts in the inventory file:

infra/inventories/beta.py
# group_name = [(host1, {'some custom': 'host data'}), ...]

my_hosts = [
    ('pyinfra.local', {
        'home_domain': 'pyinfra.local',
    }),
]  # fmt: skip

The home page deploy itself is pretty straightforward:

infra/tasks/home.py
 4
 5
 6
 7
 8
 9
10
11
12
13
from modules import nginx

sync_files = files.sync(
    name="sync files",
    src='../home',
    dest='/opt/home',
    delete=True,
)

nginx.site('templates/nginx/home.conf', host.data.home_domain)

There's one operation that synchronizes files from a local directory to a remote one, and another operation that sets up an nginx site using the domain from host data.


Except nginx.site isn't actually an operation, but a packaged deploy; besides the function definition that makes it reusable, it looks like any other deploy:

infra/modules/nginx.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@deploy("nginx site")
def site(template, domain, **context):

    write_site = files.template(
        name="write",
        src=template,
        dest=f'/etc/nginx/sites-available/{domain}',
        domain=domain,
        **context,
    )
    enable_site = files.link(
        name="enable",
        path=f'/etc/nginx/sites-enabled/{domain}',
        target=f'/etc/nginx/sites-available/{domain}',
    )

    server.service(
        name="reload",
        service='nginx',
        reloaded=True,
        _if=any_changed(write_site, enable_site),
    )

files.template uses Jinja, so we can have a base template for all HTTP sites:

infra/templates/nginx/http.conf
server {
    listen 80;
    listen [::]:80;
    server_name {{ domain }};

    {% block server %}{% endblock %}
}

...that we then extend with site-specific configuration:

infra/templates/nginx/home.conf
{% extends 'templates/nginx/http.conf' %}

{% block server -%}

    location / {
        root /opt/home;
    }

{%- endblock %}

Passing --diff shows the rendered template, which is pretty neat:

$ pyinfra inventories/beta.py deploy.py --diff
...
--> Preparing operation files...
    Loading: deploy.py
    [pyinfra.local] Will create /etc/nginx/sites-available/pyinfra.local
      @@ -0,0 +1,9 @@
      + server {
      +     listen 80;
      +     listen [::]:80;
      +     server_name pyinfra.local;
      +
      +     location / {
      +         root /opt/home;
      +     }
      + }

    [pyinfra.local] Ready: deploy.py

--> Detected changes:
    Operation                             Change              Conditional Change
    apt update                            -                   -
    install nginx                         -                   -
    disable default site                  -                   -
    reload nginx                          -                   1 (pyinfra.local)
    tasks/home.py | sync files            1 (pyinfra.local)   -
    tasks/home.py | nginx site | write    1 (pyinfra.local)   -
    tasks/home.py | nginx site | enable   1 (pyinfra.local)   -
    tasks/home.py | nginx site | reload   -                   1 (pyinfra.local)
...

And now we have a static site!

browser window showing http://pyinfra.local:2080

Web app + secrets #

The web application is a simple Flask app that we can install with pip:

$ tree app
app
├── app
│   ├── __init__.py
│   ├── static
│   │   └── style.css
│   └── templates
│       └── index.html
└── pyproject.toml

We serve it using a traditional Gunicorn being nginx on a Unix socket setup:

   :80, :443
       |
.-------------.
|    nginx    |  groups: app-socket, www-data
'-------------'
       |
    app.sock     groups: app-socket
       |
.- Gunicorn --.
|   web app   |  groups: app-socket, app
'-------------'

The deploy for it is slightly bigger, but hopefully still straightforward.

We have operations to create users and groups:

infra/tasks/app.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
server.group('app', name="create app group")
server.group('app-socket', name="create socket group")
server.user(
    name="create user",
    user='app',
    group='app',
    groups=['app-socket'],
    home="/var/lib/app",
    shell="/usr/sbin/nologin",
    system=True,
)
server.user(
    name="nginx socket access",
    user='www-data',
    groups=['app-socket'],
    append=True,
)

...to create a virtual environment with our app and its dependencies:

infra/tasks/app.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
apt.packages('python3-venv', name="install venv")
pip.venv('/opt/app', 'python3', name="create venv")
pip.packages(['gunicorn'], virtualenv='/opt/app', name="install deps")

sync_files = files.sync(
    name="sync app files",
    src='../app',
    dest='/opt/app/src',
    delete=True,
    exclude_dir=['**/__pycache__'],
)
install_app = server.shell(
    "/opt/app/bin/pip install /opt/app/src",
    name="install app",
    _if=sync_files.did_change,
)

...to create and start a systemd service for Gunicorn:

infra/tasks/app.py
56
57
58
59
60
61
62
63
64
65
66
67
68
write_service = files.template(
    name="write service",
    src='templates/systemd/app.service',
    dest='/etc/systemd/system/app.service',
)
systemd.service(
    name="enable service",
    service='app.service',
    enabled=True,
    restarted=True,
    daemon_reload=True,
    _if=any_changed(install_app, write_config, write_service),
)

...and finally to configure nginx:

infra/tasks/app.py
70
nginx.site('templates/nginx/app.conf', host.data.app_domain)

In addition, Flask needs a secret key, which usually comes from a config file:

infra/tasks/app.py
47
48
49
50
51
52
53
54
write_config = files.template(
    name="write config",
    src='templates/config.py.jinja',
    dest='/etc/opt/app/config.py',
    group='app',
    mode=640,
    get_secret=secrets.get_secret,
)

The question is, where do we store the secret key before writing the config?

The simple (and wrong) solution is to hardcode it in the deploy or in the template; unencrypted-secrets-in-version-control issue aside, we'd also like the secrets to be different between beta and prod.

As I said, a deploy is a Python file, so we can do anything: we can get them from a password manager, a security key, or even an encrypted file. To illustrate the point, here's a simple function that reads named secrets from an unencrypted JSON file:4

infra/modules/secrets.py
import json
from pyinfra import host

secrets_path = 'secrets.json'

def get_secret(name, *, stage=None):
    if not stage:
        stage = host.data.stage

    with open(secrets_path) as f:
        data = json.load(f)

    if secret := data.get(stage, {}).get(name):
        return secret

    raise RuntimeError(f"secret {name!r} for stage {stage!r} not found")

The stage comes from host data, this time not in the inventory, but in a group data file that applies to all hosts in a group (beta, the inventory name, is an implicit group):

infra/group_data/beta.py
stage = 'beta'

The config template looks like this:

infra/templates/config.py.jinja
SECRET_KEY = {{ '{!r}'.format(get_secret('app_secret_key')) }}

Deploying looks the same, only with way more operations:

$ pyinfra inventories/beta.py deploy.py
...
--> Results:
    Operation                             Hosts   Success   Error   No Change
    ...
    Grand total                           23      15        -       8
...

And now we have a web app:

browser window showing http://app.pyinfra.local:2080

HTTPS + prod #

That's a nice looking web app, but it would be even nicer if it were not "not secure". This requires HTTPS, which requires a TLS certificate.

Fortunately, those are easy to get these days – Let's Encrypt provides free certificates, and Certbot will request, install, and renew them automatically.

Unfortunately, there's no built-in Certbot operation, so we have to write a custom one:

infra/modules/certbot.py
 8
 9
10
11
12
13
14
@operation()
def certificate(domain):

    if domain in host.get_fact(Certificates):
        return

    yield f"certbot certonly -d {quote(domain)} -n --nginx --agree-tos"

Like in the initial example, we read the host state using a fact and then return commands if it doesn't match the desired state. The fact is custom too:

infra/modules/certbot.py
17
18
19
20
21
22
23
24
25
class Certificates(FactBase):
    default = dict
    requires_command = "certbot"

    def command(self):
        return "certbot certificates"

    def process(self, output):
        return {c['name']: c for c in _parse_certbot_certificates(output)}

And we already have a convenient place where we can use the operation:

infra/modules/nginx.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@deploy("nginx site")
def site(template, domain, https=None, **context):
    reload_ops = []

    if https is None:
        https = host.data.get('nginx_https')
    if https:
        reload_ops.append(certbot.certificate(domain, name="cert"))

    write_site = files.template(
        name="write",
        src=template,
        dest=f'/etc/nginx/sites-available/{domain}',
        base=f"templates/nginx/http{'s' if https else ''}.conf",
        domain=domain,
        **context,
    )

Also unfortunately, Certbot needs a resolvable domain name to work, so we can't test this using the beta virtual machine... but we can always test in prod. ( ≖‿  ≖ )

Here's a new inventory with two hosts, each with its own domain and HTTPS enabled:

infra/inventories/prod.py
my_hosts = [
    ('pyinfra.andgravity.com', {
        'home_domain': 'pyinfra.andgravity.com',
    }),
    ('app.pyinfra.andgravity.com', {
        'app_domain': 'app.pyinfra.andgravity.com',
    }),
]  # fmt: skip
infra/group_data/prod.py
stage = 'prod'
nginx_https = True

I say we just go for it:

$ pyinfra inventories/prod.py deploy.py --yes
... stuff happens ...

One minute later, there are ✨ two new websites ✨ on the internet:5

browser window showing https://pyinfra.andgravity.com
browser window showing https://app.pyinfra.andgravity.com

Questions? #

Coming soon :)


Anyway, that's it for now.

Learned something new today? Share it with others, it really helps!

Want to know when new articles come out? Subscribe via email to get new stuff straight to your inbox!

  1. And you're bound to have extra dev and pre-prod machines anyway. [return]

  2. Say you have a memory leak during a high traffic period and need to reload stuff once in a while until you can find the root cause. [return]

  3. Deploy deploy code runs once per host, so you could have different branches on different hosts within the same run. [return]

  4. Which you absolutely should not do, use keyrings.cryptfile instead. [return]

  5. Well, not anymore, there's no point in paying to host this :) [return]

November 03, 2026 04:00 PM UTC


Django Weblog

Django security releases issued: 6.1.3, 6.0.10, and 5.2.19

In accordance with our security release policy, the Django team is issuing releases for Django 6.1.3, Django 6.0.10, and Django 5.2.19. These releases address the security issues detailed below. We encourage all users of Django to upgrade as soon as possible.

CVE-2026-108147: Potential memory exhaustion in MultiPartParser via base64-encoded file upload

When using django.http.multipartparser.MultiPartParser, multipart uploads with Content-Transfer-Encoding: base64 could be buffered entirely in memory, regardless of FILE_UPLOAD_MAX_MEMORY_SIZE, before being passed to upload handlers, potentially consuming excessive memory.

This issue has severity "low" according to the Django Security Policy.

Thanks to Ignacy Kujawa for the report.

CVE-2026-111211: Potential log injection via unescaped geometry form values

django.contrib.gis.forms.GeometryField and its widgets logged invalid submitted values directly, allowing control characters (e.g. newlines or ANSI escape sequences) to be written unescaped into logs. This could enable log injection or forgery, letting attackers manipulate log appearance or structure, especially in logs processed by external systems or viewed in terminals.

This issue has severity "low" according to the Django Security Policy.

Thanks to Aaron chen for the report.

CVE-2026-111362: Server-side request forgery via crs member of GeoJSON geometries

Earlier versions of the GeoJSON spec allowed specifying a coordinate reference system (CRS) by URL. Since GDAL still resolves these URLs, any validation, save, or query path where a geometry is instantiated from user input allowed an attacker to submit a malicious CRS that would make a network request as the Django process user, or read a file, or both.

Lookups against spatial fields, the GeometryField form field, and GEOSGeometry now reject GeoJSON geometries with CRS references to external resources. The DataSource and LayerMapping utilities for loading trusted data continue to accept these values.

This issue has severity "moderate" according to the Django Security Policy.

Thanks to Submersion Security Research Team for the report.

CVE-2026-113593: Potential exposure of private data via empty and missing Vary headers

UpdateCacheMiddleware and cache_page() generated the same cache key for a request that omitted a header listed in the response's Vary header and a request that sent that header with an empty value. As a result, a response cached for one of these requests could be served to the other, which could lead to the exposure of private data.

Cache keys now distinguish between absent and empty header values, as required by RFC 9111#section-4.1.

Responses previously cached for requests that omitted a header listed in Vary will no longer be matched, and will be cached again on the next request.

This issue has severity "low" according to the Django security policy.

Thanks to Andrew Gonzalez for the report.

CVE-2026-113733: Password hash disclosure to "view only" admin users with custom user models

Admin users with the view (but not change) permission to a user model were displayed the entire password hash when the model was not AUTH_USER_MODEL, or when AUTH_USER_MODEL inherited its password field from a concrete parent model through multi-table inheritance.

This issue has severity "low" according to the Django Security Policy.

Thanks to Aaron chen for the report.

CVE-2026-114582: Server-side request forgery via SpatialReference string input

SpatialReference passed string input to GDAL, which by default resolves URLs and reads files. This behavior was not documented by Django. Other affected methods included GEOSGeometry.transform(), OGRGeometry.transform(), GDALRaster.transform(), the srs setter of OGRGeometry, and the srs and srid setters of GDALRaster. A URL or file path reaching any of these would issue an outbound request or read a file as the Django process user. The attack vectors could be combined to reveal local file contents by observing outbound requests.

SpatialReference now disables network and file access when parsing string input on GDAL 3.9 and later. As a consequence, file paths and URLs are no longer accepted as SpatialReference inputs. Dictionary references using the DICT: prefix, including those prefixed with ESRI::, are rejected on all GDAL versions.

Full protection requires GDAL 3.9 or later. Integer SRIDs are looked up directly in the EPSG database, so prefer them when the input is untrusted. Or, if strings must be used on lower GDAL versions, compare to an allow list. The new gis.W001 system check warns when the installed version of GDAL is not at least 3.9.

This issue has severity "low" according to the Django security policy.

Thanks to Adraca for the report.

Affected supported versions

Resolution

Patches to resolve the issue have been applied to Django's main, 6.1, 6.0, and 5.2 branches. The patches may be obtained from the following changesets.

CVE-2026-108147: Potential memory exhaustion in MultiPartParser via base64-encoded file upload

CVE-2026-111211: Potential log injection via unescaped geometry form values

CVE-2026-111362: Server-side request forgery via crs member of GeoJSON geometries

CVE-2026-113593: Potential exposure of private data via empty and missing Vary headers

CVE-2026-113733: Password hash disclosure to "view only" admin users with custom user models

CVE-2026-114582: Server-side request forgery via SpatialReference string input

The following releases have been issued

The PGP key ID used for this release is Jacob Walls: 131403F4D16D8DC7

General notes regarding security reporting

As always, we ask that potential security issues be reported via private email to security@djangoproject.com, and not via Django's Trac instance, nor via the Django Forum. Please see our security policies for further information.

November 03, 2026 03:30 PM UTC


Python Software Foundation

Announcing new Security Developer at the PSF

We’re excited to announce that Brett Smith is joining the Python Software Foundation as a Security Developer.

Brett will be working closely with Seth Larson, Security Developer in Residence, and Mike Fiedler, PyPI Safety and Security Engineer, on the PSF’s security team to address the increasingly demanding scale of security response for the Python ecosystem. We’re grateful to Anthropic for their generous support, which has made the critical new Security Developer role possible.


Brett brings a holistic approach to software security. Throughout his career he’s done both software development and Linux administration to support collaborative communities. He’s worked at nonprofits like the World Wide Web Consortium (W3C) and Software Freedom Conservancy as well as on open source projects like Request Tracker and the Arvados bioinformatics platform.

Security Response at Ecosystem Scale

At PyCon US 2026, Seth Larson and Mike Fiedler presented that 2026 was the “year of more” when it comes to security: the Python Security Response Team (PSRT) is processing 3-4× the volume of reports and CVEs for Python and pip compared to just a year ago, and the amount of malware published and removed from the Python Package Index (PyPI) is up 60% compared to last year. The complexity of securing the vast Python ecosystem is growing along with the scale, particularly in the face of the evolving role of AI in both security risks and security response. Providing prompt security response services and innovating to prevent future threats at the scale of an ecosystem like Python is more difficult now than ever before.

Brett’s addition to the PSF positions us to better meet this growing challenge. Having Brett on the team will both build capacity for security work the PSF already does and enable new, proactive work: He’ll help us keep pace with critical ongoing work such as vulnerability remediation, providing vulnerability data as a CVE Numbering Authority, and removing malware from PyPI. He’ll also give the team bandwidth for new projects such as scaling PyPI’s autonomous quarantine program to reduce manual effort on malware remediation and streamlining processes for the PSRT to handle the new vulnerability report volume. Stay tuned for what’s coming next on the PSF security roadmap by subscribing to the PSF blog or newsletter.

About the Python Software Foundation

The Python Software Foundation (PSF) is a non-profit whose mission is to promote, protect, and advance the Python programming language, and to support and facilitate the growth of a diverse and international community of Python programmers. The PSF supports the Python community using corporate sponsorships, grants, and donations. Are you interested in sponsoring or donating to the PSF so it can continue supporting Python and its community? Check out our sponsorship program, donate directly here, or contact our team! 

November 03, 2026 08:29 AM UTC


Tryton News

Tryton News November 2026

After the Tryton 8.2 release landed in October, the community turned to the work that follows up. The product category search responds to parent categories again, and the documentation got some improvements.

For an in depth overview of the Tryton issues please take a look at our issue tracker or see the issues and merge requests filtered by label.

Changes for the User

Stock, Production and Shipments

When you search for a product category by name, the parent categories are matched as well.

New Documentation

The stock documentation now describes the procedure for setting lots on assigned moves.

The sale credit limit module documentation has been refreshed to match the latest behaviour and configuration options.

The move grouping wizard now has its own section in the accounting documentation.

New Releases

We released bug fixes for the recently released 8.2 minor series and the currently maintained long term support series 8.0, 7.8, and 7.0.

Changes for Implementers and Developers

The product category now extends the tree() domain instead of overriding it. Overriding could mask the contribution of the tree() mixin to search_rec_name, so anyone who reuses the search override pattern should leave the tree() clause extension in place.

The party replacement now takes an optional WHERE clause to restrict the rows that are updated. The clause is provided by a hook on the model, so developers can keep certain rows (such as moves that have already been voided) untouched while the rest of the party field is rewritten.

Initial draft powered by Minimax-M3. Curated and finalized by human hands.

1 post - 1 participant

Read full topic

November 03, 2026 07:00 AM UTC


Python Bytes

#503 It’s not 85% HTML

<strong>Topics covered in this episode:</strong><br> <ul> <li><strong><a href="https://mkennedy.codes/posts/your-github-language-bar-is-lying-and-how-to-fix-it?featured_on=pythonbytes">Your GitHub language bar is lying (how to fix it)</a></strong></li> <li><strong><a href="https://engineering.fb.com/2026/10/27/developer-tools/python-typing-survey-2026-ai-tools-checker-landscape/?featured_on=pythonbytes">Python Typing Survey 2026: developers mix type checkers, and AI is writing the hints</a></strong></li> <li><strong><a href="https://www.flaskvibe.com/?featured_on=pythonbytes">Flask Vibe - Build Apps You Can Understand</a></strong></li> <li><strong><a href="https://tildeweb.nl/~michiel/python-on-my-wrist.html?featured_on=pythonbytes">Python on my wrist: MicroPython on a PineTime</a></strong></li> <li><strong>Extras</strong></li> <li><strong>Joke</strong></li> </ul><a href='https://www.youtube.com/watch?v=o63T94KiWng' style='font-weight: bold;' data-umami-event="Livestream-Past" data-umami-event-episode="503">Watch on YouTube</a><br> <p><strong>About the show</strong></p> <p>Sponsored by us! Support our work through:</p> <ul> <li>Our <a href="https://training.talkpython.fm/?featured_on=pythonbytes"><strong>courses at Talk Python</strong></a></li> <li>Consulting from <a href="https://sixfeetup.com/?featured_on=pythonbytes"><strong>Six Feet Up</strong></a> <strong>Connect with the hosts</strong></li> <li>Michael: <a href="https://fosstodon.org/@mkennedy">Mastodon</a> / <a href="https://bsky.app/profile/mkennedy.codes?featured_on=pythonbytes">BlueSky</a> / <a href="https://x.com/mkennedy?featured_on=pythonbytes">X</a> / <a href="https://www.linkedin.com/in/mkennedy/?featured_on=pythonbytes">LinkedIn</a></li> <li>Calvin: <a href="https://sixfeetup.social/@calvin?featured_on=pythonbytes">Mastodon</a> / <a href="https://bsky.app/profile/calvinhp.com?featured_on=pythonbytes">BlueSky</a> / <a href="https://x.com/calvinhp?featured_on=pythonbytes">X</a> / <a href="https://www.linkedin.com/in/calvinhp/?featured_on=pythonbytes">LinkedIn</a></li> <li>Show: <a href="https://fosstodon.org/@pythonbytes">Mastodon</a> / <a href="https://bsky.app/profile/pythonbytes.fm">BlueSky</a> / <a href="https://x.com/PythonBytes?featured_on=pythonbytes">X</a> Join us on YouTube at <a href="https://pythonbytes.fm/stream/live"><strong>pythonbytes.fm/live</strong></a> to be part of the audience. Usually <strong>Tuesday at 7am PT</strong>. Older video versions available there too. Finally, if you want an artisanal, hand-crafted digest of every week of the show notes in email form? Add your name and email to <a href="https://pythonbytes.fm/friends-of-the-show">our friends of the show list</a>, we'll never share it.</li> </ul> <p><strong>Michael #1:</strong> <a href="https://mkennedy.codes/posts/your-github-language-bar-is-lying-and-how-to-fix-it?featured_on=pythonbytes">Your GitHub language bar is lying (how to fix it)</a></p> <p>GitHub's language bar comes from Linguist, which counts every tracked byte in the repo. Michael's chameleon-rs project read as 75.5% HTML because committed docs-site output and test suites outweighed the Rust engine. Three .gitattributes attributes changed the story: linguist-generated for build output, linguist-vendored for an upstream test suite, and linguist-detectable=false for tests and benchmarks on both sides. The bar now shows the real mix, roughly 62% Rust and 38% Python.</p> <ul> <li>That bar is one of the first things people see on your repo; if generated files skew it, a few lines of .gitattributes tell the truth.</li> <li>The bar is byte-proportional, so a 9 MB committed docs build can make a Rust project read as mostly HTML</li> <li>linguist-generated hides build output and collapses those diffs in pull requests too</li> <li>linguist-vendored covers copied-in code; linguist-detectable=false drops anything from the stats</li> <li>chameleon-rs went from 75.5% HTML to roughly 62% Rust and 38% Python after a few lines of config</li> <li>Tests and benchmarks were excluded symmetrically, on both the Rust and Python sides, so neither skews the ratio</li> <li>Check patterns before pushing with git check-attr; GitHub recalculates a few minutes after the push</li> </ul> <p><strong>Calvin #2: <a href="https://engineering.fb.com/2026/10/27/developer-tools/python-typing-survey-2026-ai-tools-checker-landscape/?featured_on=pythonbytes">Python Typing Survey 2026: developers mix type checkers, and AI is writing the hints</a></strong></p> <ul> <li>Meta's 2026 Python Typing Survey results: 1,794 responses, Aug 5 - Sep 2. Pyrefly (Meta), JetBrains, Microsoft and Astral's ty team helped write it.</li> <li>Typing is stable: 85.5% use hints often or always (88.0% in 2024). Which checker people use is not stable.</li> <li>49.9% run two or more checkers (46.0% in 2025), and 83.3% of Pyrefly users also run another one. Speed is the top reason for picking a checker (56.5%).</li> <li>Checkers disagree: 33.3% report inconsistencies between them, rising to 42.7% among multi-checker users.</li> <li>AI is writing the types: 42.5% had AI write hints, and 38.4% say types make AI code more reliable (18.5% last year).</li> <li>29.8% are "not sure" whether types help AI-assisted coding, about 4x the uncertainty on any other benefit.</li> <li>Help sources flipped: Stack Overflow 46.4% to 22.9% and LLM chat 26.0% to 41.7%, 2024 to 2026.</li> <li>Top pain is still missing library types (65.1%; Django, SQLAlchemy, pandas, NumPy). The most-wanted feature is better Callable syntax (45.7%). Raw data zip is linked.</li> </ul> <p><strong>Michael #3:</strong> <a href="https://www.flaskvibe.com/?featured_on=pythonbytes">Flask Vibe - Build Apps You Can Understand</a></p> <p>Flask Vibe is a new tutorial site from Frederick Tubiermont, a developer who started coding at 47 and builds with Flask, PostgreSQL, raw SQL, and vanilla JS. The pitch: AI assistants generate more reliable, easier-to-verify code when the stack has no build step, no ORM, and no hydration. It ships a manifesto, tutorials on modular vanilla JS and AI-friendly coding patterns, and a Flask plus psycopg2 CRUD walkthrough.</p> <ul> <li>For vibe coders burned by Next.js defaults on Lovable or Replit, this is a concrete, opinionated case that simpler Python stacks pair better with AI-generated code.</li> <li>Claims a structural 1.8x LCP win over Next.js on the same dashboard app: 905ms vs 1.66s, both scoring Lighthouse 100</li> <li>Tutorials cover modular vanilla JS, AI-friendly coding patterns, and Flask CRUD with raw psycopg2 SQL and no ORM</li> <li>Creator started Python at 47 in 2022 and runs the site on the stack it teaches; the homepage is 42 KB with Lighthouse 100</li> <li>Solo London-based project (adsyme Ltd), launched via an r/flask self-post in February 2026; early, opinionated, and the benchmark leans on one self-run comparison with its methodology published on site</li> </ul> <p><strong>Calvin #4: <a href="https://tildeweb.nl/~michiel/python-on-my-wrist.html?featured_on=pythonbytes">Python on my wrist: MicroPython on a PineTime</a></strong></p> <ul> <li>Michiel (Python Leiden user group) runs MicroPython on a PineTime, Pine64's ~$26 open smartwatch: 64 MHz Cortex-M4, 64 KB RAM, 512 KB flash.</li> <li>He revived wasp-os, a dormant MicroPython watch OS stuck on a 2019 MicroPython fork whose maintainer is asking for new maintainers (issue #536).</li> <li>The build is now one command with uv. Bluetooth flashing uses bleak instead of the deprecated gatttool, cutting update time from over 8 minutes to about 3.5.</li> <li>Free RAM at boot went from 7.1 KB to 12.4 KB by using the flash chip's real 4 KB blocks. He also added a watchdog, plus vibration, touch and I2C dead-battery fixes.</li> <li>His three upstream PRs (frozen native code, PWM, ADC sample time) are merged, so wasp-os now runs on mainline MicroPython. They're targeted for MicroPython 1.30, planned for December.</li> <li>New faces: a Dutch week clock, and an 8-bit pixel clock that draws its digits without any font data and shows a live Unix epoch counter.</li> <li>The Guido app: a 16-color, 80x80 run-length-encoded portrait (photo by Kushal Das, CC BY-SA). All four images total under 12 KB, versus 112 KB for one raw screen.</li> <li>Next up is the PineTime Pro (AMOLED, GPS, dual-core M33), with no release date yet. Code is on the pinetime-upstream-micropython branch of mbeijen/wasp-os.</li> </ul> <p><strong>Extras</strong></p> <p>Calvin:</p> <ul> <li><strong>PyPI now caps version strings at 128 characters —</strong> https://blog.pypi.org/posts/2026-10-23-version-length-limit/</li> <li>Wagtail Space — https://wagtail.org/wagtail-space-2026/</li> </ul> <p>Michael:</p> <ul> <li>Thanks Hugo: <a href="https://hugovk.dev/blog/2026/cybersecurity-awareness/?featured_on=pythonbytes">Lots of Python releases for Cybersecurity Awareness Month</a></li> <li><a href="https://talkpython.fm/meetups/talk-python-in-production-building-your-own-infrastructure?featured_on=pythonbytes">Talk Python Meetup: Talk Python in Production: Building Your Own Infrastructure</a></li> </ul> <p><strong>Joke:</strong> <a href="https://x.com/pr0grammerhum0r/status/1955872046539260290?s=12&featured_on=pythonbytes">Binging red buttons</a></p>

November 03, 2026 03:41 AM UTC


Wingware

Wing Python IDE Version 12.1 - November 3, 2026

Wing 12.1 is now available. It introduces guided autonomous development. You specify the goals, answer questions, and review results. AI agents design and write the code, unit tests, and documentation. Wing keeps you in control. Wing Pro 12.1 now supports Claude Code, Codex, Antigravity, Copilot, Cursor, goose, OpenCode, and other ACP-compatible AI coding agents. It also adds a new Modern look and display themes, graphical revision control history, a redesigned New Project dialog, GitHub integration, and many other improvements.

Wing 12.1 Screen Shot

Downloads

IMPORTANT Wing 12.1.2 can't be installed as an update to the Wing 12.1 betas, so please download and install it from the links below. Then use Check for Updates from Wing's Help menu after starting Wing so that you have the latest hot fixes.

Please try it and email us if you find problems or have suggestions!

Wing 12.1.2 -- Full-featured Python IDE with two licensing tiers

Wing 101 12.1.2 -- Free IDE for teaching beginners to program

Wing 12.1 will replace Wing 12.0 when it is installed. You can revert to Wing 12.0 by reinstalling it at any time.

To get started quickly with autonomous development in Wing 12.1, see Getting Started with Autonomous Development.

New in Wing 12.1

Autonomous Development with the Tasks Manager

The new Tasks Manager is an agent that runs the Tasks tool's queues for you, so that development proceeds autonomously under your direction. You write tasks, mark the ones you want started as Ready, and review the results.

The Manager accepts anything from a bug report to a high-level specification for a new software project. It can break down larger tasks into a design and development plan. It decides task order, serializes those that are likely to edit the same files, and runs closely related tasks in shared session context. It can message a running agent to tell it about related work or new insights, or redirect it if it goes astray.

While the Manager is on, tasks run autonomously: Rather than stopping to ask questions or present intermediate results, each works as far as it can and ends with a report of the judgement calls it made along the way, and any questions it has left. A new Questions tab in the Tasks tool collects everything that is waiting for you, so you can provide answers in one place. Follow-up work and problems that agents notice as they run are added to the Plan tab of the Tasks tool as new tasks. The Manager may automatically start found tasks that are closely related to ongoing work; others wait for your review.

You stay in control of which work runs, how much the Manager can do, and how results are reviewed. Every change the Manager makes is listed and can be reverted or revised, even after changes have been committed. You can also talk to the Manager to ask about the status of development or give it special instructions about your development workflow.

Claude Code and Codex are best for autonomous development, since they self-check the safety of commands that they run. Antigravity, Copilot, and Cursor also work but don't implement the same level of safety checks. See AI Agent Safety in the AI Agent-Assisted Development chapter of the Wing manual for details and safety recommendations.

The Manager agent itself and queues in the Tasks tool can each use a different agent. You bring your own subscriptions or API keys for the agents you choose. Wing does not install agents or provide access to models.

Your Choice of AI Agent

Wing 12.1 adds support for other AI agents, now including Claude Code, Codex, Antigravity, Copilot, Cursor, goose, and OpenCode, either using their Textual User Interfaces (TUIs) or the Agent Client Protocol (ACP). AI agents can also set up how your code is run, such as a new program's main entry point, its run arguments, and its Python environment.

Improved User Interface

Modern Look: An optional new Modern look redesigns the toolbar, Preferences dialog, status bar, and tab overflow, and improves the display font. Two new display themes, Night Flight and Solo Flight, have been added. Themes can now incorporate transparent colors, be imported from VS Code, created from a small set of colors, or exported from your current color configuration.

New Project: The New Project dialog has been redesigned as a single page. Its Simple view asks only for the essentials, with a new directory, a virtualenv in the project's .venv, and a git repository as defaults, while All Options shows everything else. Progress is shown as a checklist in the dialog, and a failed step can be retried. Support for uv, Poetry, pipenv, micromamba, and pixi environments has been improved, and Wing offers to install a missing uv, Poetry, or pipenv.

Editor: Editors have a new Show Rendered toggle for Markdown, reST, HTML, SVG, images, and PDF files. The editor's scroll bar shows marks for diffs, search matches, and errors and warnings.

First Start: The first start of Wing is simpler, with a shorter Set Up Wing dialog and a Welcome to Wing window that leads with New Project, Open Folder, and Clone from GitHub. In Wing Pro and Wing Classic, trials now start automatically, with no sign-up, and all the license dialogs have been merged into one.

Improved Revision Control Integration

A new Project Log view in the version control tool shows the whole repository's history for git and Mercurial as a branch graph, with expandable diffs. You can sign in to GitHub, clone from a list of your repositories, and publish a new project to GitHub from New Project. Push Changes in the Project Status view pushes your work, and Merge From Branch on the toolbar merges another branch, in each case with an AI agent resolving any conflicts. The editor can also show annotations with commit message tooltips.

Other Improvements

Tasks Tool: Goals can be set aside for Later, and the Focus mode keeps just one goal open at a time. A new Incoming view collects tasks that agents found or proposed, for quick triage, and the Up Next view on the Execute tab shows what runs next in every queue. New Checklists hold recurring reviews, fixes, and release and deployment steps. A task's committed changes can be reverted, edited, or commented on, and every task has a short ID that links to the task wherever it is cited. Tasks can be sent to Wing on other machines, set up as peers, for example to check or fix something on another OS. Agent sessions and the Tasks Manager continue on their own once a usage limit resets, and Send Note sends a message into a running task's session.

Other Changes: Source analysis is faster and its database is about 30% smaller. Wing offers to install Python if it finds no supported version, and to remove large cache directories left behind by older versions. Accept Debug Connections has moved to Project Properties so it is set per project. Wing 12.1 also includes many bug fixes, particularly for remote development and Docker containers, debugging free-threaded Python on Windows, git worktrees and submodules, non-ASCII text on Windows, unit test discovery, and display themes.

See the change log for details.

Getting Started with Autonomous Development

To get started quickly with autonomous development in Wing 12.1, see Getting Started with Autonomous Development.

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

November 03, 2026 01:00 AM UTC

November 02, 2026


Rodrigo Girão Serrão

Migrating to Python 3.15

This practical guide tells you what to pay attention to when migrating to Python 3.15.

This short article walks you through the main things to keep in mind when migrating your code to Python 3.15:

15 days of Python 3.15

To celebrate the release of Python 3.15, over the next 15 business days I'll be writing about a new 3.15 feature every day. Explained clearly and with examples so you don't have to sift through the changelog.

Subscribe to receive this free email series:

If you want, you can also check the schedule of the upcoming emails.

3.15 migration guide

When a new Python version is released and you're considering of adopting it, you need to think about three disjoint sets of features in the context of your project. The first two are the set of new features that are backwards-compatible with other Python versions and the set of features that are not backwards-compatible. To understand the third set of features that's relevant, take a look at the following screenshot from python.org:

Timeline of active Python versions, showing 3.10 is now end-of-life and 3.11 is the lowest active version.Timeline of the lifecycle of Python versions

With the release of Python 3.15, Python 3.10 officially reached end-of-life and 3.13 entered the security-only stage. This means that Python 3.10 won't be updated anymore and Python 3.13 won't be improved; it'll only receive security updates. If you're working on...

November 02, 2026 03:51 PM UTC

November 01, 2026


Hugo van Kemenade

Lots of Python releases for Cybersecurity Awareness Month

This Cybersecurity Awareness Month, I made seven Python releases, when two were originally planned for October.

Altogether, we made 16 Python releases, when five were originally planned. Of the extra releases, only 3.15.0rc3 was not a security release.

The releases #

Announcements #

Thanks #

Huge thanks to the Sovereign Tech Agency’s Fellowship for supporting my work on all these releases.

Also thanks to the other release managers Savannah Ostrowski (3.16), Thomas Wouters (3.12 and 3.13) and Pablo Galindo Salgado (3.10 and 3.11), to Ned Deily for the all macOS builds, and to Seth Larson for helping coordinate all this security.


Header image: Computer Security Awareness Day, 1990, from the National Library of Medicine, no known copyright restrictions.

November 01, 2026 08:30 PM UTC


LernerPython blog, from Reuven Lerner

Agentic coding means we’ll need some new abstractions

I’ve been teaching programming (mostly Python) for more than 30 years. But in about a year of coding with AI, I haven’t defined a single data structure or database schema — and I’m accomplishing more, in less time, with fewer bugs, than I ever thought possible. Agentic coding is revolutionary, but it’s part of a larger story that has played out over more than 70 years. In this essay, I look at the evolution of programming languages as successive waves of higher levels of abstraction. I consider what that meant for teaching and learning coding, and how that should influence what we teach as AI becomes the dominant form of programming.

When computers were first invented, there was no such thing as “programming.” Each computer was a special-purpose machine, engineered to solve specific problems. If the problem changed, or if the computer didn’t solve it correctly, you had to make physical changes to the computer. “Debugging” meant cutting wires and soldering parts.

Then some smart people realized that we could separate the computer (the “hardware”) from the instructions and data used to solve the problem (the “software”). No longer did you have to think in terms of wires and logic gates; you could think in terms of instructions and information — verbs and nouns, represented as high and low electrical voltages, 1s and 0s, which we know today as “binary digits,” or “bits.”

Bits were the programmer’s alphabet. Just as we can use letters to write both verbs and nouns in English, programmers could use bits to give the computer instructions and then to also provide the values that the instructions acted on. But programming using bits was hard to learn, not to mention tedious and error-prone.

The power of abstraction

Early programmers quickly adopted the principle of abstraction, combining smaller pieces and processes under a single concept. Abstraction allows us to think and communicate at a higher level. And because our minds can only handle so many things at once, abstraction also allows us to solve larger problems.

Imagine that you’re a traffic planner, trying to understand the ramifications of replacing a traffic light with a traffic circle. You’ll think about the problem in terms of cars. But “car” is an abstraction: If you had to consider all of the parts and processes from each of the vehicles in the intersection, you would never be able to think clearly about the traffic patterns. Communicating your thoughts about those changes would be nearly impossible, because you would be bogged down by low-level details.

The abstraction that programmers adopted became known as assembly language, instructions they could use to control a computer. Programming in assembly was still hard, but it was easier than working with raw binary digits. Moreover, these commands had semantic power, allowing you to think in terms of mathematical operations (e.g., addition and multiplication) and operations within the computer (e.g., moving a value from the CPU to memory). Chip manufacturers competed on the basis of not only speed, but also what high-level instructions their chips supported.

With higher-level programming abstractions, we also got new techniques:

Higher-level languages, higher-level techniques

Why stop with one layer of abstraction, when you could have two? Higher-level languages, such as Fortran, Lisp, PL/I, and (eventually) C, gave us even more semantic power. They looked something like English, requiring translation (“compilation”) into machine code before they could execute. (Fine, Lisp doesn’t exactly look like English.) But the real power of these languages was that both the instructions and the data were using higher-level abstractions than assembly language provided:

These functions and data structures were ultimately stored as bits in the computer’s memory. But these higher-level functions and data structures allowed you to hide those underlying details, making it possible to tackle increasingly larger problems. Moreover, these higher-level languages lowered the bar to programming even further, allowing a larger number of people to try their hand at solving problems using their computers. However, some of them (e.g., C) still required that we manage the computer’s memory manually.

With these higher-level languages, we started to develop new techniques, including functional and object-oriented programming, which made it easier to create higher-level abstractions for both instructions and data. New languages, such as Smalltalk in the 1970s and C++ in the 1980s, made it possible to use these techniques in our own code.

Python (and friends) arrive on the scene

Starting in the late 1980s and early 1990s, we got a bunch of even higher-level languages, including Tcl, Perl, Python, and Ruby. These languages were originally seen not as full-fledged languages, but rather as providing the “glue” that allowed you to connect different parts of a computer system, as well as automate processes more easily. They were thus dismissed by many serious programmers, called “scripting languages” to emphasize their non-seriousness.

But it turns out that these languages were anything but toys. Yes, their syntax was typically easier to understand than others, and it was easier to jump in and start coding because of their dynamic typing and automatic memory management. But their main advantages were the high-level abstractions they provided:

These higher-level abstractions — along with supportive communities and easy-to-handle syntax — introduced many newcomers to the world of programming. And they inspired new languages, such as JavaScript, which includes many of the same ideas.

What to teach, what to learn

When I started my CS education in the fall of 1988, most universities taught their intro course in C. To some degree, they did so because it was the most popular language in industry. But they also taught it because C provides a relatively thin layer over assembly code. There was a strong feeling among faculty that to learn programming, you need to start from the lowest-level basics, working your way up to higher-level, more abstract programming languages. They argued that you can only really understand object-oriented and functional programming if you have a solid base of low-level C coding.

But MIT had a very different perspective: They saw programming as a combination of mathematics, philosophy, and linguistics, and taught it that way. Our intro class, known internally as 6.001 and to the rest of the world as Structure and Interpretation of Computer Programs, used Scheme, a version of the Lisp language, which was (unusually) both a very high-level language and a very old one. 6.001 focused on high-level data structures, functions, and how a programming language really works behind the scenes. We learned about variable scoping and bindings, functions as first-class objects, and object-oriented programming.

Never did we think in terms of bits and bytes. Or assembly language. Or memory management. Or what chip we were even using. The focus was on the high-level principles of programming, remaining at that high level of abstraction.

They weren’t trying to say that low-level knowledge is bad or useless. (Another course, 6.004, gave us the chance to learn that topic.) They were, however, trying to say that in the modern world of programming, it’s more important to understand high-level abstractions — how they work, and how to combine them — rather than the underlying machine code.

Nearly 20 years ago, MIT decided to switch away from Scheme for their intro class, to use a language that reflected their high-level approach but was also popular in industry. The language? Python. Thousands of MIT students have thus learned to program with a high-level, dynamic language with rich data structures, first-class functions, support for object-oriented programming, and a warm community of fellow users. Universities around the world have followed MIT’s lead, contributing in no small part to Python’s immense popularity.

How (and what) should AI-native coders learn?

A little more than one year ago, I was convinced that AI could help us code, but that we were a long way off from AI writing code for us. I was wrong; I’ve been using Claude Code and other AI coding systems since January, often for 6-8 hours each day, and I continue to be floored by how much I can get done. I’ve created oodles of functionality for LernerPython, some open-source projects, and a number of things that might turn into online businesses.

For now, I believe that I’m only able to get so much done with Claude Code because of my many decades of programming experience. But my experience here has to do with engineering systems — understanding what the different parts of my program need to do, how to separate them, how to put in guardrails so that I can be sure the software works now and in the future, among other things. I haven’t once modeled a data structure, created a new class, defined a database schema, or implemented a login system, although I have approved specs authored (and then implemented) by Claude Code.

This feels weird. And satisfying. And raises numerous questions about what we should be teaching the next generation of programmers.

I’m convinced that we’re seeing the birth of a new, higher level of abstraction. Just as Python developers don’t need to worry about what instructions are supported by their computer’s chipset, AI-native programmers — people who learn to code through AI, rather than adding AI to skills they already have — don’t need to learn about integers vs. floats, strings vs. bytestrings, or even lists vs. dictionaries, at least not at the beginning. What we long regarded as high-level data structures are now below the abstraction barrier, irrelevant to many coders until they decide to peek behind the curtain.

For now, I still believe that learning to code the old-fashioned way, getting exposed to data structures, functions, and other techniques, is useful. But within a few years, you’ll be able to have a successful coding career without knowing how to implement a dictionary or what “hashable” even means.

The best universities will teach these topics, and the best coders will learn them, because understanding the underlying layers, breaking through the abstraction barriers, can inform and influence your thinking. But just as you can get a coding job in 2026 without knowing the ins and outs of how an operating system works, or how much data your CPU can retrieve in a single clock cycle, you’ll be able to get a coding job in 2036 without knowing the ins and outs of how a programming language works.

Am I arguing against my own courses and products? Not at all: My courses are for people who want deep understanding, exercises, and a chance to ask questions during live office hours. And I still think that knowing Python and Pandas is a great asset in today’s world. I’m adding office hours and courses about AI, which I see as complementary to the existing content, rather than replacing it.

What AI-native coders will need to learn

I increasingly think that people learning programming will need to learn more system-level thinking. They won’t need to know how to define a database, or the different kinds of indexes we might use, but they will need to know that databases exist, and that different types (relational vs. document) can affect performance and data integrity. They’ll need to know that programs can read from a variety of Internet APIs — and how to do so safely and securely. They’ll need to know how to provide an API for others to read from, and to consider the security implications of doing so. They’ll need to think about designing UIs using a combination of textual specifications, screenshots and sketches — followed by many iterations.

They’ll need to think about different document formats. About sending and receiving e-mail. About accessibility. About internationalization and localization. About what happens in the browser vs. on the server. About what languages we want to use, and why.

Most importantly, they’ll need to learn how to ensure that AI is writing solid, reliable code, both now and as it gets modified, extended, and debugged over time. This means teaching them, from the get-go, about automated testing, code coverage, mutation testing. Not to mention backups, frequent commits to Git, and extensive logs. And also security — making sure that any API, site, or app they create is as secure as possible from bad actors.

Compilers are deterministic, consistently turning the same source code into the same executable. Many programmers argue that AI is problematic because it’s fundamentally non-deterministic, and will produce different solutions each time it gets the same specification and instructions. I would counter that people, who currently write the majority of the world’s code, are also non-deterministic — and yet we’ve somehow managed to create incredible software over the years. But it’s true that we shouldn’t just trust AI to do the right thing all of the time.

I’m starting to put together a completely new “AI coding for non-programmers” course, which I’ll offer both on LernerPython.com and to companies around the world. (I’ll announce it to my Better Developers list when it’s ready.) I’m already playing with the ideas of what verbs and nouns an AI coder will need to know and understand in order to be effective. It won’t focus on Python, but rather on the problems that people and companies have, and how they can fix those problems using coding.

Every time programming has added a layer of abstraction, we’ve become able to solve larger and more complex problems, and more people have been able to join the fold of programmers. I believe that AI coding will do both — but it will radically change what coders need to learn, and how we teach them. I’m excited to take on this challenge, and look forward to discussing it with others in the industry.

The post Agentic coding means we’ll need some new abstractions appeared first on LernerPython.

November 01, 2026 03:20 PM UTC

Pandas multi-index: Working with 3D data using `.loc` and `.xs`

Pandas multi-index: Working with 3D data using `.loc` and `.xs`

My students used to ask if Python Pandas supports 3D data structures.

Answer: No. Series are 1D and data frames are 2D.

But… a multi-index effectively gives you more dimensions. You can then navigate the indexes with .loc and .xs.

The post Pandas multi-index: Working with 3D data using `.loc` and `.xs` appeared first on LernerPython.

November 01, 2026 07:00 AM UTC


The Python Coding Stack

Notes from the Near Future • The Permanently Curious Stack

Things are changing around us. Quickly. Some fields are ahead of others, but all are moving in the same direction.

A year and a half ago, many programmers were intrigued by how AI could help them but most were confident they’d still be the ones writing most of the code.

Not any more.

In my work, AI tools now handle implementation better and faster than I could on my own. But I’m still in the driving seat.

I now spend most of my day interacting with AI agents. But I’m not lounging by the beach while my agents do stuff for me. I’m collaborating with my agents. Managing them. Guiding them. Bouncing ideas off them. Reviewing their work. Steering them.

It’s impossible to disentangle my contributions from my agents’ contributions when you see the output. It’s all intertwined. It’s a collaboration.

And since the AI agents are doing all the implementation, I can focus on the bigger picture, the bigger ideas, the creative aspects. I’m enjoying this process.


That’s why The Python Coding Stack today becomes:

Notes from the Near Future • The Permanently Curious Stack

I’ll still write about Python. That won’t change. The Python Coding Stack is now a section of Notes from the Near Future.

But I won’t write only about Python. That’s why this publication is changing, just like the world around it.

I’ll write about how I’m adapting to this new way of working. What I like about it. And what I don’t. What I’m experimenting with. What works and what doesn’t.

This applies to everyone, not just those interested in programming and Python.

You may also have noticed the Agents Unpacked section in recent months. The change in what I write about is not starting now. It started a few months ago as I found myself spending more and more time with agents rather than with Python (directly). This section is where I’ll focus on how AI agents work and what they are, since we all need to learn how these tools work. And I’m learning in public in this section. I’ll also write about my experiments and experiences using agents.

You’ll also see an Agent & Co. section in the menu. You won’t receive these posts in your feed by default, but you can turn them on if you wish! Twice a week, I’ll share a fun perspective about human-agent interactions. Think of it as a day in the life of Monty – that’s my agent’s name. But behind the light-hearted nature of these cartoons and comics, you’ll gain an appreciation of the realities of working with agents. There’s plenty to learn from these posts!

Here’s the first comic, published earlier today:

How about brain atrophy? Will I lose my sharpness by handing the coding over to agents? As you’ll see from my articles over the coming weeks, I don’t think that’s the case. Working with AI is not about being lazy but about focusing on other aspects of problem-solving.

But I don’t want to get rusty with coding, a skill I enjoy. That’s why I’m starting to learn Rust. So, there’s another new section here on Notes from the Near Future where I’ll be learning Rust in public. There’s nothing published in Getting Rusty yet, but expect the series to start before the end of this year.

By the way, you can customise which sections you receive. But I’d suggest exploring the new sections for a bit first, especially if you’re an AI-sceptic!

So here’s the new line-up for you on Notes from the Near Future • The Permanently Curious Stack:

Subscribe now

I have some more ideas, too. But that’s enough change for one day.

This publication is three and a half years old. There are now over 9,000 subscribers, and that number is still growing daily. One constant in this publication has been that I’ve always published articles that I would want to read myself. That won’t change. In fact, that’s why this publication is broadening its focus.

Another constant throughout the years has been that I’ve always published the majority of my work for free, fully accessible to all. That won’t change, either. But I do publish content exclusively for premium subscribers, and some of my upcoming series will be available only for premium subscribers. It takes plenty of work to produce a publication such as this and keep it running for so long. The support from premium subscribers helps keep this publication thriving.

As always, feel free to reply to this post and any other post. Tell me what you’re working on, how your work is changing (or not), share your thoughts, views, anything, really.

See you in the next post or somewhere in the online world before then…

Stephen

Subscribe now

stephengruppetta.com

November 01, 2026 12:33 AM UTC

October 31, 2026


The Python Coding Stack

Thoroughly Checked

Subscribe now

stephengruppetta.com

October 31, 2026 11:46 PM UTC


Jaime Buelta

Software is a discovery process

The development of software is a relatively new discipline, and one that is constantly changing. The last couple of years have been an earthquake with the arrival of AI, but even before that, the field changed beyond recognition from one decade to the next. Still, there’s something that I think is part of its essence, and it hasn’t changed. It has been widely recognised for the last quarter of a century, since the Agile Manifesto. It is borderline impossible to know exactly what functionality a software project needs until it’s done. And... Read More

October 31, 2026 06:13 PM UTC


LernerPython blog, from Reuven Lerner

Pandas droplevel: Remove a level from a multi-index

Pandas droplevel: Remove a level from a multi-index

To remove a level from a Python Pandas multi-index, use droplevel:

– “level” kwarg takes an int or string
– “axis” kwarg can be “index” or “columns” — or use 0 or 1, but I prefer the names

Returned data frame has the same number of rows, but index might have duplicates.

The post Pandas droplevel: Remove a level from a multi-index appeared first on LernerPython.

October 31, 2026 07:00 AM UTC


Seth Michael Larson

Extracting Billy Hatcher “Joycarry Game” ROMs with Python

Billy Hatcher and the Giant Egg contains a feature called “Joycarry Games” similar to Animal Crossing’s NES game “Advance Play” feature. I wanted to extract the ROMs for these mini-games just like I did for Animal Crossing. Doing so wasn’t as straightforward; the ROMs aren’t available directly in the disc filesystem like they are in Animal Crossing. To extract the ROMs I ported an existing Rust program to Python (“rewrite-it-in-Python”, anyone?) that receives and saves the ROMs from the game running in Dolphin using the GBA TCP and Joyboot protocols.

These games are simple, and I only really played one as a kid: Puyo Pop. The game is similar to “Dr. Robotnik’s Mean Bean Machine”, which was a favorite of mine. This was the only way I could take that game “on-the-go”, often downloading the game to my GBA before road trips to see my grandparents.


Joycarry Game menu in Billy Hatcher with “ChuChu Rocket! Challenge” selected

The Python program is a port of this MIT licensed Rust program by Icedude907 with added functionality to save the EWRAM data after the ROM has been transmitted to the program via GBA TCP. I’ve distributed this program via a GitHub Gist.

Using the program requires the Dolphin emulator and dumping the ROM of the Billy Hatcher GameCube disc you intend to download Joyboot ROMs from. In the case of Billy Hatcher you also need to have unlocked the five different eggs hidden in levels. The fastest way to get around this is to download a 100% save file for your game’s region and place the file into Dolphin’s save file folder.

To extract the ROMs, launch Dolphin and set the controller in port 4 to be “GBA (TCP)”; Billy Hatcher in particular only works if the GC-to-GBA link cable is plugged into port 4. Start Dolphin playing Billy Hatcher, navigate to the world map, and then select “Joycarry Game”. From here you can select each game to be downloaded to our “GBA”, and the program should display information about receiving the ROM data.

Language setting and Joycarry ROMs

Something I did not know was that two of the five Joycarry ROMs, “ChuChu Rocket! Challenge” and “Puyo Pop”, have translations for both English and Japanese. Which Joyboot ROM translation is sent to you depends on the game region and which language is selected in the menu.


“ChuChu Rocket! Challenge”


“Puyo Pop”

The NTSC region (GEZE8P) of the game only supports English (no language selector in options) and only has English Joycarry ROMs. The NTSC-J region (GEGJ8P) supports Japanese and English, sending the corresponding Joycarry ROM depending on which language is selected. The PAL region (GEZP8P) supports English, French, German, Spanish, Italian, and ... Japanese! I checked each language, and the English Joycarry ROM is sent for all languages except Japanese.

Here are the SHA256 checksums of each ROM by game title and language:

ROM Title Language SHA256
CHU2 AGB EN 131258b587b4724ea06a5a0fbdce4d6ec05c8e63ef6b10a6000dfa8efd57c435
CHU2 AGB JP 5adc33400eead9ce3d327377b7ce150a48e632abc2494370acb0b6ed61c1337a
NIGHTS EN 944fadb8fac3abf8c7db38f837677be3d3a664c56e6c8eb58e9e1949a10ac5f9
BILLYHATCHER EN 128afd1c40a65831bd013c2c91abdadd816155df2b1a5456a7af6d260d48108e
BILLYHATCHER EN 2af4d14151981c2341c193cd2cc3d38e10dee1336ebedcd264f8e8bcb5e1f87e
MINNADE PUYO EN e769bd190bee6735a97cd239f587e3921108073a6f20ab20b8d62ba9c447cc12
MINNADE PUYO JP 521f6a2a0876f2685205c0cfbfde934c14b8b3a6b5c173f4aa9fdfe93aed80a5

Happy Halloween! 🎃👻



Thanks for reading ♥ I would love to hear your thoughts! Contact me via Mastodon, Bluesky, or email. Browse the blog archive. Check out my blogroll.



October 31, 2026 12:00 AM UTC

October 30, 2026


PyCharm

Summary

Available fixed versions

The issue is fixed in the following versions:

The 2025.2.6.* updates for all IDEs now contain only the JBR fix. They will get a separate hotfix for the JCEF problem. Until then, use a workaround to disable JCEF in your IDE (see instructions below).

IDE versions 2025.1 and earlier, and Toolbox App 3.7 and earlier, will not receive this fix. You must update to a fixed version.

Details

In October 2026, the private keys of eight intermediate certificates issued by the certificate authority eMudhra were compromised. An attacker who can intercept your network traffic could use them to impersonate websites and services.

Full details of the incident.

JetBrains IDEs check certificates against the trust store of their bundled runtime, JetBrains Runtime (JBR), so updating your operating system doesn’t protect against this issue. We’ve integrated Oracle’s fixes for CVE-2026-91403 and CVE-2026-114820 into JBR as-is. The updated runtime blocks the compromised certificates and no longer trusts these root certificates:

In the fixed versions of 2025.3 and later, the embedded browser now receives Chrome’s certificate updates, which block the compromised certificates (CVE-2026-118696). It downloads them from Google’s update servers shortly after the IDE starts. If your network blocks access to Google’s update servers, the embedded browser stays unprotected. In this case, turn it off as described below.

The Toolbox App and JetBrains IDEs also install a background service, jetbrainsd. Its list of trusted certificates is built into the service itself, so updating JBR doesn’t fix it. We’ve released a separate fix in jetbrainsd 0.9.9728, tracked as CVE-2026-118697. Only one copy of jetbrainsd runs for each user, and a newer version always replaces an older one. Updating any app that includes jetbrainsd 0.9.9728 fixes it for all JetBrains apps.

What you should do: Update now

The fix is available in the latest versions of JetBrains IDEs, from 2025.2 up to 2026.3 RC. The upcoming 2026.3 release will include it too. Updating to the latest bugfix release of your current version is enough. Versions 2025.1 and earlier will not receive a fix. If you use one of them, please update to one of the versions above.

If you use ReSharper or other JetBrains .NET tools, update them to the latest version.

Important for Toolbox App users: versions 3.7 and earlier won’t receive this fix. If you’re on 3.7, update to version 3.9.1 or 3.8.2.

If you use JetBrains Gateway, update it to one of the following versions: 2026.1.4, 2025.3.3, 2025.2.6.

If you use JetBrains Runtime directly, update to one of the fixed builds: jbr25.635.79, jbr25.508.41, jbr25.329.142, jbr21.1163.125, jbr21.1038.91.

In rare cases, the IDE will no longer connect to a server whose certificate chains to one of the removed root certificates. The server’s owner will need to replace the certificate.

If you can’t update right away

Clean your certificates manually

The Java runtime bundled with these IDEs and Toolbox App still trusts two eMudhra-related root certificates. If you can’t update the IDE, give it a cleaned copy of its certificate list and turn off its built-in browser, which has its own list that can’t be changed. It takes about five minutes and is fully reversible.

  1. Copy the certificate list from the IDE folder to your home folder:
    macOS: <IDE>.app/Contents/jbr/Contents/Home/lib/security/cacerts
    Windows / Linux: <IDE>/jbr/lib/security/cacerts
     
  2. Remove the two affected roots, using the keytool in the IDE’s jbr/bin folder:
    keytool -delete -keystore <copy> -storepass changeit -alias "comodoaaaca [jdk]"
    keytool -delete -keystore <copy> -storepass changeit -alias "emsigneccrootcag3 [jdk]"
     
  3. Open Help | Edit Custom VM Options, add this line, then restart the IDE:
    -Djavax.net.ssl.trustStore=<full path to the copy>
    Important: write the full path, with no ~, no variables, and no quotes. A wrong path silently leaves you unprotected.
     
  4. In Settings | Tools | Server Certificates, make sure “Accept non-trusted certificates automatically” is off.
     
  5. Linux only: update the ca-certificates package.

What to expect: If the IDE shows an “untrusted server certificate” dialog for an eMudhra certificate, click Reject.

Disable JCEF in your IDE

To workaround the JCEF problem in 2025.2.6.4 and previous versions, or in any version if your network blocks Google’s update servers:

  1. Open Search Everywhere -> Actions
  2. Type “Registry”
  3. Open Registry
  4. Search for ‘ide.browser.jcef.enabled’ key
  5. Switch it off
  6. Restart

Note: Turning off the embedded browser affects features that use it, for example, Markdown and HTML previews and Jupyter notebook output. Some plugins may also stop working correctly. After you update to a fixed version, turn ide.browser.jcef.enabled back on and restart the IDE.

Update jetbrainsd background service

The Toolbox App and JetBrains IDEs use a background service, jetbrainsd. It has its own built-in list of trusted certificates, so updating JBR doesn’t fix it, and the workarounds above don’t apply to it.

The fix is in jetbrainsd 0.9.9728. You get it when you install or update a fixed version of the Toolbox App or an IDE and start it. Only one copy of jetbrainsd runs for each user, and the newest version replaces older ones. So updating the Toolbox App fixes jetbrainsd for all your IDEs, even if you can’t update the IDEs themselves.

If you can’t update the Toolbox App or an IDE, update jetbrainsd manually:

  1. Download the archive for your platform and check its SHA-256 checksum with shasum -a 256 (macOS), sha256sum (Linux), or Get-FileHash (Windows PowerShell):
  2. Unpack the archive and run jetbrainsd once without arguments. It replaces the older version. To unpack, use tar -xzf. On Windows 10 and later, tar is built in.
    • Linux: ./jetbrainsd/bin/jetbrainsd
    • macOS: ./jetbrainsd.app/Contents/MacOS/jetbrainsd
    • Windows: .\jetbrainsd\bin\jetbrainsd.exe
       
  3. Check the installed version. It should be 0.9.9728 or later:
    • Linux: "${XDG_DATA_HOME:-$HOME/.local/share}/JetBrains/Daemon/bundles/current/jetbrainsd/bin/jetbrainsd" --version
    • macOS: "$HOME/Library/Application Support/JetBrains/Daemon/bundles/current/jetbrainsd.app/Contents/MacOS/jetbrainsd" --version
    • Windows (PowerShell): & "$env:LOCALAPPDATA\JetBrains\Daemon\bundles\current\jetbrainsd\bin\jetbrainsd.exe" --version

Also check

Your project JDKs and any tools that bring their own runtime, such as Node.js, Python, or Go, use their own trust stores. Update them as their vendors recommend.

Support

If you have any questions about this issue or have trouble updating, please contact our Support team.

October 30, 2026 07:42 PM UTC


EuroPython

Humans of EuroPython: Sangarshanan

This week we’d love to thank Sangarshanan, a member of the Communications Team at EuroPython 2026.

Unofficially, Sang holds the title of EuroPython’s CMO - Chief Meme Officer. If you’ve ever laughed at our memes, read our posts or watched some of our short videos, you’ve seen for yourself that Sang makes EuroPython unique.

Thank you for your work, Sang!

Cover image of Sangarshanan, a member of the Communications Team at EuroPython 2026

EP: Before you volunteered this year, had you already been involved in EuroPython?

Yes! I have been volunteering almost every year since 2021.

EP: Is there a task you took care of that most attendees probably don&apost realise goes on behind the scenes at EuroPython?

Probably all of the memes that you see on our social media! I really love engaging with the community that way.

EP: Were there any surprising or amusing moments during your time volunteering?

So much that I could start a podcast! But one recent moment that comes to mind is when I accidentally scheduled a social media post that auto transcribed someone saying Krakow as Crack Out. This was flagged and removed by our attentive team but I always appreciate how the community handles my - or actually anyone&aposs - mistakes with so much grace, warmth and humor. We even have inside jokes about all the mistakes we made along the way. 

EP: What was the biggest surprise for you about volunteering?

How much it is about the people and not really about what you do! If you are working with wonderful, kind, amazing people, any work: mundane or creative becomes so much more fun and fulfilling, and this is the essence of the entire EuroPython community.

EP: What&aposs a small, everyday skill (not a technical one) that volunteering sharpened for you?

I have become much better at interacting with people since I started volunteering because you need to be patient, quick on your feet, and help solve problems onsite. Also being with the communications team, I had no idea how small things like the timings of the social media posts or the title of our blogs affect engagement, I am still learning a lot about all this which is quite cool and interesting.

EP: What makes you keep coming back to volunteer year after year?

This answer gets repeated a lot but it&aposs so true: The people, a lot of whom have become really good friends over the years. An excuse to hang out with them online and meet them in person once a year is good enough for me to volunteer year after year.

EP: Thank you for all your work, Sang!

October 30, 2026 06:39 PM UTC


Rodrigo Girão Serrão

Python abi3t: what is it?

Learn what the abi3t and the limited C API are and what that means for Python users.

This article explains what the limited C API and the abi3t are to Python developers who don't have experience with C extensions.

15 days of Python 3.15

To celebrate the release of Python 3.15, over the next 15 business days I'll be writing about a new 3.15 feature every day. Explained clearly and with examples so you don't have to sift through the changelog.

Subscribe to receive this free email series:

If you want, you can also check the schedule of the upcoming emails.

Python wheels

A Python wheel is the binary file that you download from PyPI every time you “pip install” a Python package. Conversely, it's also the binary file you build when you're getting ready to publish your project to PyPI so that others can use it.

If you use uv to create a packageable project, cd into it, and then build it, you'll notice that uv builds a Python wheel for you:

% uv init --package pkgtest
Initialized project `pkgtest` at `~/pkgtest`
% cd pkgtest
~/pkgtest % uv build
Building source distribution...
Building wheel from source distribution...
Successfully built dist/pkgtest-0.1.0.tar.gz
Successfully built dist/pkgtest-0.1.0-py3-none-any.whl

The bottom line of output says that uv was successful in building the wheel pkgtest-0.1.0-py3-none-any.whl.

A Python wheel has the extension .whl and the three last segments of the file name are the compatibility tags: {python tag}-{abi tag}-{platform tag}. These compatibility tags determine on what systems and environments the wheel can be installed:

Compatibility tag Meaning
Python tag Python implementation and version required
ABI tag What ABI is required, if any...

October 30, 2026 04:27 PM UTC


PyCharm

Compromised eMudhra Certificates: Update Your JetBrains IDEs, ReSharper, .NET Tools, Toolbox App, and Gateway Now

October 30, 2026 03:24 PM UTC


LernerPython blog, from Reuven Lerner

Pandas MultiIndex: What a two-level index looks like

Pandas MultiIndex: What a two-level index looks like

What does a Python Pandas data frame’s multi-index look like?

This:

MultiIndex([(0.0, 1),
            (1.0, 1),
            (2.0, 1),
            (3.0, 1),
            (4.0, 1),
            (5.0, 1),
            (6.0, 1)],
           names=['passenger_count', 'VendorID'])

The post Pandas MultiIndex: What a two-level index looks like appeared first on LernerPython.

October 30, 2026 07:00 AM UTC


"Michael Kennedy's Thoughts on Technology"

Your GitHub language bar is lying (how to fix it)

tl;dr; GitHub’s language bar counts every byte in your repo, including generated docs and test suites. A few lines in .gitattributes tell it what to skip. For my project, that took it from “75% HTML” to roughly 62% Rust and 38% Python, which is what the project really is.


You know that colorful bar on every GitHub repo’s page, the one that says what the project is written in? I just learned you can tell it what to count. On one of my projects, it made a big difference.

Why does GitHub think my Rust project is HTML?

My project chameleon-rs is a Rust re-implementation of the Chameleon template engine, shipped as a Python package (I wrote about it here). Most of the engine is Rust, with a Python layer for the public API and framework integrations. Here’s what GitHub said it was:

HTML? Three quarters of it? There’s barely any hand-written HTML in the whole repo. Yes, it’s a templating engine, but the templates live in the tests and docs.

The documentation site was to blame. I build the docs with Great Docs and commit the output to site/ so it can be served straight from the repo. That’s about 9 MB of generated HTML, JavaScript and CSS, and GitHub counted every byte of it as if I’d written it by hand.

How does GitHub decide what language a repo is?

The bar comes from Linguist, GitHub’s language detection library. Linguist goes over every tracked file, works out its language, and adds up the bytes. More bytes, bigger slice. It already skips some things on its own, like well-known vendor folders, minified files and common generated files. But it has no way to know that your site/ folder is build output.

You tell it with a .gitattributes file at the root of your repo. It’s the same file Git uses for line endings and diff settings, and Linguist reads a few attributes of its own from it:

Each line is a path pattern followed by attributes. Think of a .gitignore line with some settings tacked on the end.

Round one: the docs

Two lines took care of the worst of it:

site/** linguist-generated
tests/upstream/** linguist-vendored

The second line covers Chameleon’s own test suite, which I vendor unmodified to measure compatibility. It’s not my code, so it shouldn’t count either.

After pushing, the bar read 55.9% Python, 43.3% Rust. Much better, but still not right.

Round two: the tests

The engine is mostly Rust, so why was Python winning? I measured the bytes:

Where Size
Python package 420 KB
Python tests 685 KB
Rust, everything 950 KB

The Python tests were bigger than the Python package! It’s nice to have a thorough test suite. But it was making a Rust engine look like a Python project.

The tests are coming out too. To keep things fair, I excluded the Rust tests and benchmarks along with the Python tests, otherwise I’d just be skewing it the other way. Here’s the final file:

# GitHub's language statistics (Linguist) skip these: the Great Docs build output (`make docs`)
# is generated, and Chameleon's test suite is vendored unmodified.
site/** linguist-generated
tests/upstream/** linguist-vendored

# Tests and benchmarks aren't the product: the Python tests outweigh the Python package, so
# counting them makes a mostly-Rust engine read as mostly Python. The Rust ones are left out
# too, to measure both languages alike (inline `#[cfg(test)]` modules still count).
tests/** linguist-detectable=false
crates/*/tests/** linguist-detectable=false
crates/*/benches/** linguist-detectable=false
crates/chameleon-core/src/tests.rs linguist-detectable=false

You can check your patterns before pushing with git check-attr:

git check-attr linguist-generated linguist-detectable -- site/index.html tests/test_api.py

Where did it end up?

The bar should land around 62% Rust and 38% Python. That matches what chameleon-rs is: a Rust engine with a Python API on top. GitHub recalculates a few minutes after you push to the default branch.

That bar is one of the first things people see when they land on your project. If yours is off because of a docs build, vendored code, or a big test suite, it’s a few lines of .gitattributes to fix. I’m off to do the same for some of my other projects now.

October 30, 2026 05:19 AM UTC

October 29, 2026


Rodrigo Girão Serrão

Thread-safe iterators

Learn how to write and use thread-safe iterators in Python 3.15.

This article shows how to write and use iterators in a thread-safe manner using the new tools from Python 3.15:

15 days of Python 3.15

To celebrate the release of Python 3.15, over the next 15 business days I'll be writing about a new 3.15 feature every day. Explained clearly and with examples so you don't have to sift through the changelog.

Subscribe to receive this free email series:

If you want, you can also check the schedule of the upcoming emails.

Iterators are not thread-safe

Most iterators are not thread-safe by default. For example, generator iterators can raise a ValueError if accessed simultaneously from two different threads, as you'll see now.

First, you're going to define a slow generator:

import time

def slow_counter():
    idx = 0
    while True:
        time.sleep(1)
        idx += 1
        yield idx

counter = slow_counter()

The generator counter takes a second to produce each successive number. Then, you're going to write a function that is going to try to consume a value out of the counter in a dedicated thread:

def count_consumer(name, counter):
    try:
        idx = next(counter)
    except ValueError as e:
        print(f"ValueError in {name}: {e}")
    else:
        print(f"{name} got {idx = }")

The function count_consumer will run in two different threads at the same time. The idea is that the second thread will try to fetch a value out of counter while the first thread is still waiting for the first slow result.

By passing count_consumer to two different threads, you'll see that the second thread triggers an exception:

t1 = threading.Thread(target=count_consumer, args=("t1", counter))
t2 = threading.Thread(target=count_consumer, args=("t2", counter))

t1.start()
t2.start()

t1.join()...

October 29, 2026 05:09 PM UTC


Python Software Foundation

Announcing PSF Community Service Award Recipients!

The PSF Community Service Awards (CSAs) are a formal way for the PSF Board of Directors to offer recognition of work which, in its opinion, significantly improves the Foundation's fulfillment of its mission to build a vibrant, welcoming, global Python community. These awards shine a light on the incredible people who are the heart and soul of our community– those whose dedication, creativity, and generosity help the PSF fulfill its mission. The PSF CSAs celebrate individuals who have been truly invaluable, inspiring others through their example, and demonstrates that service to the Python community leads to recognition and reward. If you know of someone in the Python community deserving of a PSF CSA award, please submit them to the PSF Board via psf@python.org at any time. You can read more about PSF CSA’s on our website. 

The PSF Board is excited to announce 5 new CSAs, awarded to Abhijeet Mote, Elena Williams, Hugo van Kemenade, Kevin Horn, Sarah Abderemane, for their contributions to the Python community. Read more about their work and impact below. 

Abhijeet Mote

Abhijeet Mote has spent more than a decade teaching Python and expanding access to technical education across Asia, Europe, and North America. In 2019, he founded the Python Penang Meetup, which he led until 2022 and which grew into one of Malaysia's most active developer communities, giving students, engineers, and enthusiasts a place to learn and share knowledge. In India, he has trained university faculty, students, and professionals, and he works with universities to design curricula that integrate Python, AI/ML, and data engineering. In the United States, Abhijeet leads community Python classes at public libraries, and he also mentors students in rural India remotely, offering guidance and encouragement to young learners with limited access to technology.

Abhijeet's contributions extend to the international Python community and to academic research. He has spoken and led workshops at conferences including at PyCon X Italy and PyCon US, and he supports researchers and PhD scholars in using Python, Pandas, and PyTorch to turn theoretical work into working experiments. On the open source side, he has contributed to GNUKhata, advises the open-source education initiative FOSSLipy, has served as a technical reviewer for several Python books, and maintains open repositories that have helped learners worldwide. Through the AWS Community Builders program, he also promotes Python best practices in cloud and DevOps. In 2025, he was named a Fellow of the Python Software Foundation, and he serves on the PSF Diversity & Inclusion Working Group. Abhijeet is known for his humility, empathy, and consistency. He has opened doors for learners at every stage through his work in classrooms, libraries, conferences, and open-source projects across three continents.

Elena Williams

Elena Williams has spent well over a decade building Python communities in Australia and beyond. In 2011 she co-founded PyLadiesAU, and in 2012 she founded the Canberra Python User Group, which she still helps organize. She went on to help run the Perth Python User Group in 2013 and co-founded Sydney PyLadies in 2014. Elena has also been a core organizer of PyCon AU, with a history of involvement in DjangoCon AU and DjangoCon US, and she is a regular speaker at Python conferences.

Elena's Django Girls work has had a particularly far-reaching impact. She organized the second Django Girls event ever held, and the first without the program's founders, then went on to organize Django Girls events in Brisbane, Perth, Sydney, Canberra, and Adelaide, as well as the first in North America. Her Perth event, which was combined with a Rails Girls workshop, directly gave rise to SheCodes, an award-winning national organization that is now ten years old, and her Django Girls work has helped spark other substantial diversity-in-tech organizations in Australia. Today, Elena continues to teach, support, and mentor people across Python communities, especially in the Django world, where she serves as a moderator on the Django Discord and as a member of the Django Code of Conduct committee. She is also involved in open source at the national level in Australia as a council member of Linux Australia.

Hugo van Kemenade

Hugo van Kemenade is a steady, ever-present contributor to the Python community, and his work spans nearly every layer of the project. He is the release manager for Python 3.14 and 3.15, a role that keeps new versions flowing to the community at every stage of the release cycle. A CPython contributor since 2017, he joined the Python Triage team in 2022, became a core developer in 2024, and has served as a PEP Editor since 2022. Hugo also chairs the monthly Documentation Working Group meetings, helping make it easier for community members to get involved, and co-organizes the Helsinki Python meetup.

Hugo's reach extends well beyond core development. He has been a maintainer of Pillow since 2014, where he modernized the codebase and helped automate its release process, and he helps maintain other widely used packages such as termcolor, humanize, and PrettyTable. He also maintains community resources that track top PyPI packages, Python version readiness, and free-threaded wheel support, giving the ecosystem useful data at just the right moment. Hugo is also a constant presence in the places where Python happens, from reviewing pull requests to answering questions on Discord and Discourse, always ready with a helpful pointer. A Python Software Foundation Fellow since 2022, Hugo's generosity with his time and expertise makes Python better for everyone who builds with it.

Kevin Horn

Kevin Horn has been a constant presence in the Texas Python community for more than fifteen years. He attended his first PyTexas conference in 2009, became an organizer in 2011, and has helped bring the annual event to the community ever since. In the early 2010s he also became an organizer of the DFW Pythoneers meetup, which he still helps run today, hosting monthly gatherings for over a decade. In 2014, he joined two others in founding the PyTexas Foundation and guiding it through the process of receiving 501(c)(3) status, and he has served as its Vice President ever since.

Kevin is always willing to mentor anyone on any topic, has guided numerous new meetup organizers and attendees, and freely shares advice on community organizing with those who ask. His encouragement has helped other organizers step into leadership roles of their own. Kevin’s warmth is also on display at PyCon US, where he has been a regular since attending his first conference in Dallas in 2006. One year, he made a point of sitting with people he didn't know at every meal, and by the end of the conference he was struggling to find a table where he didn't already know someone. That openness captures what Kevin brings to the community.

Sarah Abderemane

Sarah Abderemane has become a cornerstone of the Django community, with a journey that began with Hacktoberfest and grew into long-term maintainership. After contributing to accessibility work in the Jupyter ecosystem through Read the Docs, she implemented dark mode on the Django website, which led her to become a maintainer of djangoproject.com. She is also a co-founder of Djangonaut Space, a mentorship program that lowers the barrier to entry for new contributors to Django and its ecosystem. Her service to the Django Software Foundation includes chairing the Website Working Group, serving on the Social Media Working Group, and previously contributing to the Fundraising Working Group.

Sarah's impact extends across accessibility, governance, and global community building. As a core member of the Django Accessibility team, she co-authored the 2023 accessibility report and co-led the onboarding of four new team members in 2024. Sarah delivered a keynote on accessibility at PyCon IT and helps organize the Inclusive Web Devroom at FOSDEM. She served on the DSF Board in 2024 and 2025, including as Vice President in 2025, where she led outreach, coordinated working groups, and managed membership. Throughout, Sarah has been a steadfast supporter of the francophone Python and Django communities, including Python Cameroon, helping organizers access resources and visibility and showing up in person at their events. Her work helps ensure that Python and Django are welcoming across both languages and geographies.

October 29, 2026 09:46 AM UTC


The Python Coding Stack

3. How can words turn into actions?

In previous chapters of Agents Unpacked, we explored what makes an AI agent different from a chatbot and how the model, its tools, and the software connecting them work together.

You ask an AI agent whether you’ll need a raincoat tomorrow. To check the forecast, it can't rely on its knowledge. It needs up-to-date information. It needs to turn your question into a request that a weather service can understand.

This time, we look inside the request itself. How does a large language model (LLM) turn everyday wording into a named operation with specific inputs? And why can a request contain all the right fields yet still ask for the wrong thing?

Who’s Monty? Monty is my personal AI agent. Read the introduction to this series.

Subscribe now

In conversation • Chapter 3

Monty:

Suppose you ask an assistant:

“Will I need a raincoat in Oxford next Saturday?”

It has a weather-lookup tool available, but “Oxford next Saturday” isn’t quite as precise as it looks.

What might the assistant need to clarify before requesting the forecast?

Stephen:

You got me here. I can't figure out why this is not precise. The only thing I'd pick is that "next Saturday" can be ambiguous. It sometimes refers to the Saturday coming up, and sometimes to the next one after the one coming up. Is that the ambiguity?

Monty:

Yes, that’s exactly the ambiguity. A specific date, such as “24 October 2026”, avoids those two readings.

I also had the place in mind. There’s an Oxford in England, but there are Oxfords elsewhere, too. Context might make the intended one obvious, so the assistant needn’t ask unnecessarily. My opening made that sound more mysterious than it was. I’ve managed to turn choosing a coat into a geography test.

The useful point is that the assistant needs to turn our everyday wording into the particular location and date it will send to the weather tool. That involves interpreting what we mean, not just copying our words.

Stephen:

Got it.

Monty:

Suppose we’ve settled on Oxford in England and Saturday, 24 October.

As we saw in Chapter 2, the surrounding software, or harness, tells the model what tools are available and what information each needs. For our weather example, a tool description could say:

get_weather: Look up a forecast. Requires a location and a date written as year-month-day.

It can then produce a request like this:

(Illustrative request, not a real lookup)

{
  "name": "get_weather",
  "arguments": {
    "location": "Oxford, UK",
    "date": "2026-10-24"
  }
}

This format is called JSON. You don’t need to learn its punctuation. Here, name identifies the tool, and arguments contains the inputs supplied to it.

Notice that the request doesn’t mention a raincoat even though that’s what your prompt asked. The model has translated your question into a request for information it can use to answer you.

Stephen:

So, let's see if I follow. The harness receives this response from the LLM and it recognises it's not text but it's a tool call. The harness has all the information it needs, so it can choose to call the get_weather tool and pass the location and date to it. The tool then fetches the weather forecast for that location and date, presumably by accessing some online service that provides this information.

Monty:

Yes, that’s the sequence for our example. The weather tool could contact an online forecasting service using those inputs.

One refinement: the JSON is still text. In a typical tool-use interface, the model’s response is explicitly marked as a tool request. The harness isn’t simply spotting curly brackets in an ordinary reply. Our mock-up represents that request, rather than showing a particular provider’s exact format.

After any checks the software implements, it can run the tool. Here’s a hypothetical successful result (not a real forecast):

{
  "location": "Oxford, UK",
  "date": "2026-10-24",
  "forecast": "Rain likely",
  "temperature_c": 12
}

That gives the model evidence for its raincoat recommendation. Notice that the forecast comes from the tool’s result, not from the model generating a plausible answer about October weather.

Stephen:

So the tool returns this output. Then the harness sends this to the model, which makes sense of it and uses it to provide text for the user. Makes sense, yes. So, to pick up on something you said earlier, does the LLM label each thing it sends to the harness as either text or a tool call, which is how the harness knows what type of output comes from the LLM?

Monty:

Broadly, yes. The model communicates through an interface that distinguishes ordinary text from requests to use tools.

Think of the response as arriving in labelled sections. A text section contains words to show the user. A tool-request section identifies the tool and supplies its inputs. The harness reads those section types rather than guessing from the wording.

The exact arrangement varies between systems. A single response can also contain both, such as “I’ll check the forecast” followed by a tool request. It needn’t be entirely one or the other.

That’s why showing you our JSON example doesn’t trigger a weather lookup. It’s inside an ordinary text reply, not sent as an actual tool request.

Stephen:

Got it. What's next?

Monty:

There’s a useful question hiding behind our example: how did the model know how to ask for the weather?

Two things come together:

The model needn’t have encountered our particular get_weather tool during training. Its description might be enough for it to use the tool, much as you can follow instructions for an unfamiliar appliance.

But that isn’t guaranteed. Understanding a description, choosing the right tool, and supplying sensible inputs can all go wrong.

Supplying a new tool description isn’t retraining the model. It’s giving the existing model information to work with.

Stephen:

So the model sees the tool as a black box. It doesn't know how it works, but it knows what it's meant to do and what it needs as input. This means that whoever creates tools needs to write a good description for the tool. However, this is not something that a user needs to worry about, right?

Monty:

Yes. The model doesn’t need to know how the tool works internally. It needs enough information to understand what the tool offers and how to request it. “Black box” is a useful way to think about that, although the model might know something about its workings.

And generally, you’re right about the user. If you’re using a ready-made assistant, its builders or whoever connects the tools should supply clear descriptions and input requirements. You shouldn’t need to write those yourself.

Your part is usually to explain what you want and clarify anything the assistant can’t reasonably infer.

But a good description isn’t a safeguard by itself. Saying “a date is required” tells the model what to provide. Software that rejects a request with no date actually checks that requirement.

Stephen:

If I recall correctly, in the early days of LLMs, when they first hit the mainstream, they could only provide text, right? Presumably tool calling came later.

Monty:

Yes, broadly, if you mean the early mainstream chat interfaces. You typed text, and they replied with text. Dedicated tool-calling support became more common later.

But text output wasn’t an absolute barrier to using tools. A developer could instruct a model:

If you need a forecast, write WEATHER_REQUEST, followed by the location and date.

Software could watch for that convention, interpret it, and run the tool. Whether the model followed the convention reliably was another matter.

Dedicated tool-calling interfaces make that distinction explicit, rather than requiring developers to invent their own conventions. Models can also be trained specifically to produce appropriate tool requests.

So it wasn’t a jump from “words” to an entirely different ability to act. Software could already treat generated words as instructions. Dedicated support made that exchange more structured, though not automatically correct.

Stephen:

And I assume the harnesses around those first chat interfaces, like the first ChatGPT, were fairly basic and mostly just relayed the text to the user and kept the messages together to form the full conversation. So, the harnesses have also evolved to allow tool calls, right?

Monty:

Yes. The surrounding software has evolved too, not just the models.

Your description captures the visible interaction in an early text-only chat product: pass the conversation to the model, receive its reply, and display it. I’d avoid calling the whole system basic, though. There were also accounts, safety measures, and other machinery behind that simple interface.

To support tools, the software needs additional responsibilities: supply tool definitions, recognise tool requests, arrange execution, and return the results. It also needs to handle whatever checks, approvals, and errors its builders choose to support.

The two sides have to fit together. A model capable of requesting tools can’t use them through software that only displays its output. Equally, adding tools to the software doesn’t guarantee that a particular model will request them correctly.

Stephen:

Understood. I'm ready to move on.

Monty:

Let’s look at what the software can actually check.

Suppose the model requests a forecast for Oxford but leaves out the date. If the software checks that all required inputs are present, it can reject that request before attempting a lookup. The model then needs to supply the missing date, asking you if necessary.

Now suppose both inputs are present:

That’s a real place and a valid date. But you meant Oxford in England.

Would checking that the request contains an acceptable location and date catch that mistake?

Stephen:

No, in this case the harness and the tool won’t catch this error. This was the model that made an assumption without checking.

Monty:

Right. Those input checks wouldn’t catch a valid location that wasn’t the one you meant. If the context didn’t justify choosing Mississippi, the model should have clarified rather than silently committing to it. Additional software checks could help, but checking the required fields alone wouldn’t.

Now suppose it requests the correct Oxford and date, but receives:

“No forecast available for that date.”

That’s different again. The request was accepted, and the service answered, but the answer lacks the information needed.

The model shouldn’t treat that as “no rain expected”. Missing information isn’t a negative forecast. It might try another suitable source or explain that it couldn’t obtain the forecast, rather than invent a raincoat recommendation.

Stephen:

That makes sense.

Monty:

Two other outcomes call for different responses:

Neither tells us what the weather will be.

Now let’s contrast that lookup with a calculation. Suppose you ask:

“Calculate the fuel cost for a 100-mile journey, assuming fuel costs 15p per mile.”

A calculator tool returns £15. Unlike the weather tool, it hasn’t fetched an external fact. It has calculated from supplied inputs.

What does that result establish, and what does it leave unconfirmed about the journey’s actual fuel cost?

Stephen:

It doesn’t check what assumptions are behind the 15p per mile estimate.

Monty:

Exactly. The calculator establishes that 100 miles at 15p per mile comes to £15. It doesn’t establish whether 15p is realistic for that car and journey, or whether the journey is actually 100 miles. Correct arithmetic can rest on inaccurate assumptions.

Stephen:

But the request from the user was clear about calculating the cost of the journey. Why would the agent need to doubt the user? It wasn’t asked to confirm that the values used make sense?

Monty:

You’re right. For that request, the agent should use the supplied assumptions and calculate £15. It wasn’t asked to investigate whether 15p per mile was realistic.

My question was about what the result proves, but I blurred that into what the agent ought to check. Those are different things. You asked for arithmetic. I’d apparently decided to conduct an inquiry.

“Using your figures, the fuel cost is £15” answers the request accurately. It needn’t challenge your figures or add a disclaimer.

The distinction matters if the agent supplies the assumptions itself, or presents £15 as a verified real-world cost rather than a calculation from your inputs.

Now consider our third example, purely hypothetically:

“Add ‘Trip to Oxford’ to my calendar next Saturday at nine.”

A calendar tool would need more precise inputs, just as the weather tool did. But there’s a new distinction: this request would change something stored, rather than retrieve information or calculate an answer.

Even if the model supplies an exact date and time, what else might need settling before that request should go ahead?

Stephen:

The harness would need to make sure that it has permission to make changes to the user’s calendar.

Monty:

Yes. A request can be correctly formed without being authorised. Software needs to check whether this system is allowed to write to that calendar. Merely showing the model a calendar tool doesn’t grant that permission.

In our hypothetical example, the user has explicitly asked for an event to be added. Whether separate confirmation is needed depends on the system’s rules, not on a universal requirement to ask again.

There are also practical details to settle: which calendar, whether “nine” means morning or evening, the time zone, and the event’s duration. Context or agreed defaults may supply those. Otherwise, the assistant should ask only for what’s missing.

Permission answers “May this change be made?” Precise inputs answer “Which change should be made?”

Stephen:

Which part of the agent deals with permission? The model or the harness? And how is permission set?

Monty:

Both have a role, but they do different jobs. The model can interpret your instructions. Software must enforce access restrictions.

The calendar service might give the connected account read-only access. The harness might also block changes or require approval. Those controls are set by whoever connects the account or configures the assistant.

Your request can authorise a particular action within those limits. It can’t turn a read-only connection into a writable one.

Stephen:

So the protection level depends on where that permission is set. If you ask the model not to write in a calendar, it may or may not do so. If the harness has rules to prevent calling the tool to update the calendar, it’s safer, but presumably a model may send a request that could bypass that protection, in theory. However, if the tool to write to a calendar is not present in the system, then nothing can possibly write to the calendar. The third option is the strictest and safest.

Monty:

Broadly, yes, but the important question is whether another route to the calendar remains open.

A properly enforced harness rule can reject a write request regardless of how the model phrases it. Sending a different request doesn’t automatically bypass that rule. A bypass would require a gap, such as another tool that can make the same change without passing through the check.

That also qualifies your third option. Removing the dedicated calendar-writing tool isn’t enough if a general-purpose tool can still contact the calendar service using credentials that permit changes.

If the system genuinely has no route capable of writing to the calendar, it can’t write to it. A read-only calendar connection provides a strong boundary because the service itself refuses writes through that connection, even if the model requests one.

We’ll explore permissions and safeguards more fully in a later chapter. For now, the key distinction is that a request can contain all the right details without the system being allowed to carry it out.

Stephen:

Got it.

Monty:

Let’s bring that back to the contents of a request.

Suppose the user has specified a one-hour “Trip to Oxford” event at 9 a.m., UK time, on 24 October, in their work calendar:

{
  "name": "create_event",
  "arguments": {
    "calendar": "work",
    "title": "Trip to Oxford",
    "date": "2026-10-24",
    "start_time": "09:00",
    "time_zone": "Europe/London",
    "duration_minutes": 30
  }
}

Suppose the tool accepts all those values and the system has permission to create events. Does this request match what the user asked for?

Stephen:

Not quite since the duration in the request sent to the tool is 30 minutes not one hour as requested by the user. But why would such an error happen?

Monty:

Exactly. The model supplied a valid duration, but not the one requested.

Generating a tool request isn’t guaranteed to be an exact translation. The model might carry over 30 minutes from an earlier example, use a familiar default instead of the stated duration, or simply make a mistake when producing that value. We couldn’t tell which happened from this request alone.

In effect, it has translated “one hour” incorrectly. The tidy structure doesn’t prevent that, just as a neatly completed form can contain the wrong answer.

A check that says “duration must be a positive number” would accept 30. Catching this error requires comparing the requested duration with the user’s instruction, not merely checking whether the tool accepts it.

Stephen:

And presumably a better LLM is less likely to make such a mistake, and more likely to call the correct tool, right?

Monty:

Yes, a model that’s better at tool use should make those mistakes less often. That includes choosing an appropriate tool, following its input requirements, and preserving details such as “one hour”.

But “better” needs to mean better at those particular tasks. A model that writes excellent prose isn’t necessarily better at using tools. Performance also depends on how clearly the tools are described and how much competing information is in the conversation.

You’d compare models on realistic tasks, checking both which tool they choose and the values they supply. A request can run successfully yet still contain the wrong duration.

So model quality matters, but a more capable model reduces the risk. It doesn’t remove the need for appropriate checks.

Stephen:

Understood.

Monty:

Let’s close the article conversation here. How an agent checks its work deserves its own conversation, rather than being squeezed into the end of this one.

Subscribe now

What we’ve unpacked in this chapter

Turning words into an action involves interpretation. The model has to choose an available tool and translate what we mean into the inputs that tool expects. A neatly structured request can still contain the wrong place, date, or duration.

The key points:

Next, we’ll move from a single request to a sequence of actions and ask whether an agent needs a plan.

Subscribe now

<< Previous Post: 2. Who actually does the work, the model or the software?

>> Next Post: Coming soon…

Table of Contents

October 29, 2026 09:27 AM UTC