Portable pandas and Python idioms

Analysis code travels between machines. The syntax that older interpreters reject, the pandas calls whose behaviour has moved between releases, the forms that behave the same everywhere, and how to check what an environment has before trusting an answer found online.

Updated 21 Sept 20262 code blocks verifiedpandascompatibilityportabilityversionsidioms

A notebook is written on one machine and run on another: a shared server, a colleague’s laptop, a container built two years ago. The cheapest way to make it survive that trip is to write to the subset of Python and pandas that behaves the same across releases, and to avoid the newest syntax and the calls whose behaviour has moved. Every snippet on this site is written that way and executed in a pinned environment before it is published; nothing here needs anything beyond pandas, numpy, scikit-learn and matplotlib.

Syntax that older interpreters reject

Newer formPortable form
match / caseif / elif
list[int], dict[str, float] as annotationsfrom typing import List, Dict
X | None in annotationsOptional[X] from typing
dict1 | dict2{**dict1, **dict2}
str.removeprefix / removesuffixslicing with a check
Parenthesised context managers with (a as x, b as y):one with per line, or nested
zip(..., strict=True)check lengths first

Safe everywhere this site cares about: f-strings, the walrus operator :=, positional-only parameters, functools.cached_property, math.prod.

# newer syntax: fails to parse on an older interpreter
def summarise(xs: list[float]) -> dict[str, float] | None: ...
merged = defaults | overrides
match kind:
    case "daily": ...
from typing import List, Dict, Optional

def summarise(xs: List[float]) -> Optional[Dict[str, float]]:
    if not xs:
        return None
    return {"n": float(len(xs)), "mean": sum(xs) / len(xs)}

defaults = {"window": 20, "min_periods": 10}
overrides = {"window": 60}
merged = {**defaults, **overrides}
print(summarise([1.0, 2.0, 3.0]), merged)

pandas calls whose behaviour has moved

AvoidBecauseWrite instead
groupby(...).apply(f, include_groups=False)the argument exists only in recent releasesselect columns before apply: g[["a", "b"]].apply(f)
DataFrame.map(f)recent addition; older releases have only applymapDataFrame.applymap(f), or df[col].map(f) per column
groupby.rolling(...) assigned back to the framethe result’s index shape has changed between releasesg[col].transform(lambda x: x.rolling(n).mean())
Series.rolling(step=...)recent additionslice after rolling: .rolling(n).mean()[::step]
pd.NA-typed nullable columns as the defaultbehaviour still shiftingstay with float64 and np.nan for numeric work
df.groupby(...).apply(f) where f uses the grouping columndeprecated, later an errorpass only the needed columns, or use .transform
read_csv(..., dtype_backend="pyarrow")recent, and needs pyarrowthe default numpy backend
OneHotEncoder(sparse_output=...) in scikit-learnthe argument was renamed between releasespd.get_dummies on the training frame, then reindex the test frame to the training columns

The transform-with-rolling pattern is the one that matters most and it is used on every page here; see rolling windows per group. The get_dummies + reindex pattern is the portable way to one-hot encode with unseen categories handled explicitly; see categorical features and unseen levels.

Check what you have

import sys, pandas as pd, numpy as np
print(sys.version.split()[0], "| pandas", pd.__version__, "| numpy", np.__version__)

This line goes at the top of the notebook. When an answer found online looks version-specific, compare it against this line before trusting it; see looking things up.

Adding a package

Install only what the analysis needs, and pin to a release that supports the interpreter in front of you, because pip will otherwise pick the newest wheel and fail to build it. pip index versions <package> lists what is installable. Write the base-stack version of the analysis first, so that if the install fails nothing is lost; the optional-library version is a bonus, labelled as such. See OLS from first principles for the pattern.

Charts

plt.savefig("fig.png", dpi=120) then insert the PNG into the slide. Plain matplotlib does everything on this site. See charts as PNGs for slides.