Python 3.15 for Data Scientists: Lazy Imports, frozendict, and Tachyon Python 3.15, released October 9, 2026, adds a lazy import keyword (PEP 810) that delays module loading until first use, cutting a test script's median startup time from 280 ms to 14 ms across 20 runs, roughly 20 times faster. The release also introduces a built-in frozendict (PEP 814) that raises a TypeError on item assignment and requires creating a new frozendict with | to change a value, preventing pipeline functions from silently mutating shared configuration. The changes matter to data scientists because they reduce startup cost from heavy libraries such as pandas and make configuration objects safe to pass between pipeline steps. Table of Contents Introduction Python 3.15 https://docs.python.org/3.15/whatsnew/3.15.html was released on October 9, 2026. The changelog is long, so this article picks the five changes you’re most likely to care about as a data scientist. For each feature, you’ll see the problem with the old way and how the new feature solves it. Stay Current with CodeCut Easy-to-digest articles on Python, AI, and open-source tools. Delivered twice a week. .codecut-subscribe-wrap { display: flex; justify-content: center; } .codecut-subscribe-btn { background: 72BEFA important; color: 2F2D2E important; border: none; border-radius: 8px; padding: 12px 28px; font-family: inherit; font-size: 16px; font-weight: 700; cursor: pointer; text-decoration: none important; display: inline-flex; align-items: center; justify-content: center; transition: background 0.3s ease; } .codecut-subscribe-btn:hover, .codecut-subscribe-btn:focus { background: 5aa8e8 important; color: 2F2D2E important; text-decoration: none important; } / Mobile responsive / @media max-width: 480px { .codecut-subscribe-btn { width: 100%; text-align: center; } } Lazy Imports: Load Libraries Only When You Use Them A normal import loads a module right when Python reaches the import line: import pandas as pd pandas loads here, even if the script never uses it print "hello" The usual workaround is to move import pandas inside the function that needs it. That works, but the imports end up spread across functions. Anyone reading the code has to check every function to see which libraries it uses. def load trips path : import pandas as pd return pd.read parquet path In Python 3.15, the lazy keyword PEP 810 https://peps.python.org/pep-0810/ lets the import stay at the top while delaying the load until the module is first used: lazy import pandas as pd nothing loads yet print "hello" df = pd.read parquet "taxi.parquet" pandas loads here, on first use To measure the difference, compare two scripts that differ only in the import line. Neither uses pandas, so the lazy script should skip loading it entirely and run much faster: normal.py import pandas as pd print "hello" lazy.py lazy import pandas as pd print "hello" Running each script 20 times and taking the median gives: Outputnormal: 280 ms lazy: 14 ms <--- 20 times faster The lazy version starts about 20 times faster. This shows that it never loads pandas. frozendict: Immutable Dicts, Built In Plain dicts https://codecut.ai/hidden-cost-python-dictionaries-safer-alternatives/ can be changed by any function they’re passed to. In a pipeline, that means one step can quietly change a setting that later steps rely on. config = {"min fare": 0, "max fare": 500} def drop outliers cfg : cfg "max fare" = 1000 meant as a local tweak print "before:", config drop outliers config print "after: ", config Outputbefore: {'min fare': 0, 'max fare': 500} after: {'min fare': 0, 'max fare': 1000} <- max fare changed Python 3.15 solves this problem with a built-in frozendict PEP 814 https://peps.python.org/pep-0814/ , a dict that can’t be changed after it’s created: config = frozendict min fare=0, max fare=500 def drop outliers cfg : cfg "max fare" = 1000 drop outliers config OutputTypeError: 'frozendict' object does not support item assignment To change a value, create a new frozendict with |. The original stays the same: strict config = config | {"max fare": 300} print "original:", config print "new: ", strict config Outputoriginal: frozendict {'min fare': 0, 'max fare': 500} new: frozendict {'min fare': 0, 'max fare': 300} Changing a setting now takes a deliberate step, so it can’t happen by accident. 📚 For comprehensive guidance on managing pipeline configs, check out Production-Ready Data Science https://codecut.ai/production-ready-data-science/ . Unpacking in Comprehensions: Flatten and Merge in One Line You can flatten a list of lists with a double for or itertools.chain.from iterable https://codecut.ai/itertools-feature-engineering-essentials/ . However, the double for is hard to read at a glance. column groups = "fare amount", "tip amount" , "trip distance" , "passenger count", "payment type" , print column for group in column groups for column in group Output 'fare amount', 'tip amount', 'trip distance', 'passenger count', 'payment type' Python 3.15 makes this more readable by allowing inside comprehensions PEP 798 https://peps.python.org/pep-0798/ . It unpacks each inner list, so the items end up in one flat list: print group for group in column groups The same syntax works for sets: monthly payment types = {1, 2}, {2, 3, 4}, {1, 5} print { types for types in monthly payment types} Output{1, 2, 3, 4, 5} allows merging a list of dicts into one. If two dicts share a key, the dict that comes later in the list overrides the earlier one. layers = {"min fare": 0, "max fare": 500}, defaults {"max fare": 300}, overrides max fare {"payment types": 1, 2 }, adds a new key print { layer for layer in layers} Output{'min fare': 0, 'max fare': 300, 'payment types': 1, 2 } Tachyon: Profile a Running Job Without Restarting It Tachyon vs cProfile A profiler tells you which parts of your code take the most time. Python’s built-in profiler, cProfile, times every function call, even when the same function runs millions of times. That extra work makes the program run slower: call 1 func 1 start timer ... stop timer call 2 func 1 start timer ... stop timer <- same function, timed again call 3 func 2 start timer ... stop timer ... every call gets timed - extra work on each one Tachyon, the new profiler in Python 3.15, doesn’t time every function call. Instead, it checks which function is running once every millisecond. A function that shows up in 3 of 4 checks used about 75% of the time: 1 ms func 1 2 ms func 1 3 ms func 2 4 ms func 1 func 1 3 of 4 checks = 75% of the time func 2 1 of 4 checks = 25% of the time Profile with Tachyon Let’s try profiling a pipeline with Tachyon. Start by installing pyarrow and downloading one month of NYC Yellow Taxi trip records https://www.nyc.gov/site/tlc/about/tlc-trip-record-data.page : uv pip install pyarrow curl -o taxi.parquet https://d37ci6vzurychx.cloudfront.net/trip-data/yellow tripdata 2024-01.parquet Create a simple data pipeline: pipeline.py import pandas as pd def load trips path : columns = "PULocationID", "fare amount", "tip amount", "trip distance" return pd.read parquet path, columns=columns def clean trips trips : return trips trips "fare amount" 0 & trips "trip distance" 0 def tip rate row : return row "tip amount" / row "fare amount" def add tip rate trips : return trips.assign tip rate=trips.apply tip rate, axis=1 def summarize zones trips : return trips.groupby "PULocationID" "tip rate" .median trips = load trips "taxi.parquet" trips = clean trips trips trips = add tip rate trips summary = summarize zones trips print summary.sort values ascending=False .head 3 To run profiling with Tachyon, run: python -m profiling.sampling run --sort cumtime -l 9 pipeline.py Add the --flamegraph option to write an interactive HTML report: python -m profiling.sampling run --flamegraph -o flamegraph.html pipeline.py Each bar is a function. Bars below it are the functions it called. A bar’s width and color show how much of the run time it took: orange-red 60% or more yellow 35% to 60% green 18% to 35% blue under 18% Hover over a bar to see its details. add tip rate takes 78.5% of the run, so it’s the slow step: add tip rate calls tip rate through pandas apply, once for every row. tip rate alone takes 54.2% of the run: The narrow stack on the right is import pandas as pd, at 18.6% of the run. The import is slow because pandas loads 293 submodules, and each one runs its own setup code: So the slow part of this pipeline is the row-by-row apply in add tip rate, not loading the data. To understand how Tachyon got these results, look at the Profile Summary panel in the sidebar. It shows that Tachyon: Collected 3.1K samples in 5.52 seconds Saw 261 different functions in those samples Succeeded on 56.7% of its checks sampling efficiency Tachyon also barely slows the program down. It added about 4% to the run time, while cProfile made it more than 3 times slower: python pipeline.py ~4.9 s no profiler python -m profiling.sampling run pipeline.py ~5.1 s Tachyon: +4% python -m cProfile pipeline.py ~16.7 s cProfile: 3.4x slower Attach to a Running Process cProfile can’t attach to a program that’s already running. To profile a long job with it, you’d have to restart the job. Tachyon can attach to a job that’s already running, without restarting it. The command below samples a running job for 5 seconds. Replace