TL;DR: Python 3.15 came out October 9, 2026, and Python 3.10 reached end of life on October 1, 2026. For network automation, the headline features are explicit lazy imports (a big win for CLI tools that import Netmiko or NAPALM), a built-in frozendict, UTF-8 as the default file encoding everywhere, and a new sampling profiler. If your scripts still run on 3.10, this is the release to plan your jump to.
A lot of network automation code is a pile of small command line tools: back up configs, check interface errors, push a VLAN, pull an inventory. They get run constantly, often from a jump host, and a lot of their runtime is spent before they ever open an SSH session. Python 3.15 has several changes that hit that kind of code directly. Here’s what matters for Python 3.15 for network engineers, with examples you can paste into your own tools.
First, the calendar: 3.10 is end of life
Per the Python devguide’s version status page, Python 3.10 reached end of life on October 1, 2026. That means no more security fixes. 3.11 follows in October 2027. Python 3.15 was released October 9, 2026 and is supported until October 2031.
If you have automation running on an old distro Python, a vendor appliance or a long-lived virtualenv, check what it’s actually using:
python3 -c "import sys; print(sys.version)"
# or, for every venv on a jump host
find / -name pyvenv.cfg -exec grep -H '^version' {} \; 2>/dev/null
You don’t need to manage system Python to try 3.15. A tool like uv can install it side by side:
uv python install 3.15
uv venv --python 3.15 .venv315
source .venv315/bin/activate
uv pip install netmiko napalm nornir ncclient
In a clean test environment on CPython 3.15.1 (details at the end), Netmiko 4.8.0, NAPALM 5.2.0, Nornir 3.6.0 and ncclient 0.7.0 all installed and imported cleanly. One thing showed up: pyeapi, a NAPALM dependency, prints SyntaxWarning: 'return' in a 'finally' block. That’s from PEP 765 (added in 3.14), and it’s a warning, not an error. Check your own dependency list rather than assuming.
Lazy imports: faster startup for automation CLIs
This is the feature to test first. PEP 810 adds a lazy soft keyword. A lazy import binds the name right away but doesn’t actually load the module until the first time you use it.
import argparse
lazy from netmiko import ConnectHandler
def main():
p = argparse.ArgumentParser(description="Back up a device config")
p.add_argument("host")
args = p.parse_args()
with ConnectHandler(device_type="cisco_ios", host=args.host,
username="admin", password="example") as conn:
print(conn.send_command("show running-config"))
if __name__ == "__main__":
main()
Why it matters: importing Netmiko pulls in Paramiko, cryptography, TextFSM templates and a lot more. If somebody runs backup.py --help, or the script exits early on a bad argument, an eager import still pays that whole cost.
What the numbers looked like
For this post, that exact script was timed in two versions, one with a normal from netmiko import ConnectHandler and one with lazy from, running --help 20 times each with a warm cache. Setup: CPython 3.15.1 installed with uv, Netmiko 4.8.0, a Linux 6.12 VM with 8 vCPUs. Results, median wall clock:
- Eager import: about 240 to 256 ms across two runs of the test
- Lazy import: about 39 to 43 ms
- Bare interpreter (
python -c pass): about 12 ms
python -X importtime put Netmiko’s cumulative import time at about 165 ms on its own. Your numbers will differ with hardware, disk and how many packages are installed. The important caveat: lazy imports don’t make the SSH session faster. When the script actually connects, it still imports Netmiko. The win is on every code path that never needs it, which in a multi-command CLI is most of them.
Rules and gotchas
lazyis only allowed at module scope. Using it inside a function, a class body or atry/except/finallyblock is aSyntaxError, and so arelazy from x import *and lazy__future__imports.- If a lazily imported module fails to load, the error is raised at first use, not at the top of the file. The traceback shows both the use site and the original import line, but a missing dependency now shows up later than you’re used to.
- Modules that do real work at import time, like registering plugins, now do that work later. Test before turning lazy imports on broadly.
Supporting 3.15 and older versions together
The lazy keyword is a syntax error on 3.14 and earlier. If a script has to run on both, use __lazy_modules__. On 3.15 the listed imports become lazy, and older versions just see a normal list and import eagerly:
__lazy_modules__ = ["netmiko", "napalm"]
import argparse
import netmiko # lazy on 3.15+, eager on older versions
import napalm # lazy on 3.15+, eager on older versions
You can also flip the default for a whole run without touching code with python -X lazy_imports=all or PYTHON_LAZY_IMPORTS=all. In the same test, running the unmodified eager script with PYTHON_LAZY_IMPORTS=all came in at a 44 ms median. For finer control, sys.set_lazy_imports_filter() lets you decide per module.
frozendict: device defaults that can’t drift
PEP 814 adds frozendict as a built-in. It’s immutable, it’s hashable when its keys and values are, and it’s not a subclass of dict. For automation that’s a clean way to hold platform defaults that nothing downstream should change:
IOS_DEFAULTS = frozendict(device_type="cisco_ios", port=22, timeout=30)
def connect_args(host, **overrides):
# | with a frozendict returns a new frozendict; the original is untouched
return dict(IOS_DEFAULTS | {"host": host} | overrides)
IOS_DEFAULTS["timeout"] = 5
# TypeError: 'frozendict' object does not support item assignment
Because it’s hashable, a frozendict also works as a dictionary key or in a set, which is handy for caching results per unique connection profile. On 3.15.1, those behaviors check out, and json.dumps() serializes a frozendict fine. One thing to watch: code that does isinstance(x, dict) won’t match a frozendict. Use isinstance(x, (dict, frozendict)) or collections.abc.Mapping if a function should accept either one.
UTF-8 is now the default encoding
With PEP 686, Python 3.15 runs in UTF-8 mode by default. open() without an encoding argument now uses UTF-8 regardless of the system locale. On Linux and macOS this usually changes nothing. On Windows jump hosts, where the old default was typically the ANSI code page such as cp1252, it’s a real behavior change.
Where network engineers will feel it: saved configs, CSV inventories and TextFSM output written with a bare open(). A config file with a non-ASCII interface description written by an older Python on Windows may have been saved in cp1252, and 3.15 will now read it as UTF-8. Netmiko already defaults to UTF-8 for its own channel encoding, so the change mostly hits your own file handling. The fix is the same advice that’s been around for years: say what you mean.
from pathlib import Path
# new files: be explicit
Path("backups/core1.cfg").write_text(config, encoding="utf-8")
# reading an old file you know was written with the Windows locale
legacy = open("old_inventory.csv", encoding="locale", newline="")
If something breaks and you need time to fix it, PYTHONUTF8=0 or -X utf8=0 turns the old behavior back on. Treat that as a short-term workaround, not a fix.
A built-in sampling profiler
PEP 799 adds a profiling package. profiling.tracing is the familiar deterministic profiler (cProfile is still there as an alias), and profiling.sampling is a new statistical sampler the docs call Tachyon, rated at up to 1,000,000 samples per second. The old pure-Python profile module is deprecated and scheduled for removal in 3.17.
For automation, the useful part is wall-clock mode. Most slow network scripts are waiting on devices, not burning CPU, and a sampling profiler in --mode wall (the default) shows where that time goes:
# profile a run and get an interactive HTML flame graph
python -m profiling.sampling run --flamegraph -o backup.html backup.py core1
# attach to a long-running collector by PID, or snapshot a hung one
python -m profiling.sampling attach 1234
python -m profiling.sampling dump 1234
Other options include --mode cpu, -a for all threads (useful for threaded Netmiko or Nornir runs), --live for a top-style view, and --async-aware for asyncio code. The dump command is the one I’d keep in my back pocket: it prints a stack for every thread of a stuck process, which is exactly what you want when a nightly backup job hangs on one device. Attaching to another process may require extra OS permissions.
Other changes worth knowing
- Frame pointers on by default (PEP 831), which makes system-level profilers work better with Python.
- Unpacking in comprehensions (PEP 798), for example
[*vlans for vlans in per_switch]to flatten a list of lists. - A built-in
sentineltype (PEP 661) for “no value given” defaults that aren’tNone. - JIT compiler improvements, still something to measure rather than assume for I/O-bound automation.
The full list is in the official What’s New in Python 3.15 page.
A migration checklist
- Inventory which Python each automation host and venv actually runs, and get anything on 3.10 onto a supported version.
- Build a 3.15 venv next to the old one and run your test suite or a dry run against lab devices.
- Search for bare
open()calls and add explicit encodings. - Read warnings at startup (like pyeapi’s) and track them upstream.
- Try
lazyimports on your slowest-starting CLI and measure with-X importtimebefore and after. - Use
__lazy_modules__if a script has to keep running on older versions.
If you’re building toward AI-assisted automation, fast, predictable CLI tools matter even more, since an agent may call them hundreds of times. I covered that side in AI Agents for Network Engineering.
The takeaway
Python 3.15 isn’t a flashy release for network engineers, but it’s a practical one. Lazy imports can cut startup time on automation CLIs that carry heavy dependencies, frozendict gives you safe shared defaults, UTF-8 by default removes a whole class of Windows encoding surprises, and the new profiler makes slow or hung jobs easier to diagnose. With 3.10 now out of support, it’s a good time to move.
Sources
- Python documentation, What’s New in Python 3.15
- Python Developer’s Guide, Status of Python versions
- PEP 810, Explicit lazy imports
- PEP 814, Add frozendict built-in type
- PEP 686, Make UTF-8 mode default
- PEP 799, A dedicated profiling package
- PEP 765, Disallow return/break/continue that exit a finally block
- Startup timings: measured October 29, 2026 on CPython 3.15.1 (uv build), Netmiko 4.8.0, Linux 6.12 VM with 8 vCPUs, 20 runs per variant, median wall clock, warm cache

No responses yet