How do you use sys.version_info to guard code that only runs on newer Python?
answer
- Ask the interpreter, never guess
- It is a tuple, so compare tuples
- Two elements, not three
- Guards protect names, not parsing
- sys.version_info >= (3, 12)
basics
~10 sCompare sys.version_info against a plain tuple, as in sys.version_info >= (3, 12), and put the newer-only import or branch inside that guard. Never parse the sys.version string; tuple comparison is exact, cheap and unambiguous.
solid answer
~40 s`sys.version_info` is a tuple-like value of `(major, minor, micro, releaselevel, serial)`, so the idiomatic check is a direct tuple comparison: `if sys.version_info >= (3, 12):`. Compare against a two-element tuple, because stdlib features land per minor release, not per patch. The guard chooses between two code paths at import time, so it can protect a **name** — an import, an attribute, an argument added in a later release — but it cannot protect **syntax**: the whole module is compiled before any line of it runs, so a `match` statement inside a 3.10 guard still raises `SyntaxError` on 3.9. When you care whether a feature is present rather than which interpreter you are on, `try: ... except ImportError:` is the more honest test, since it also succeeds when a backport supplies the name.
code
python · 12 linesimport sys
if sys.version_info >= (3, 11):
import tomllib
def load_config(data: bytes) -> dict:
return tomllib.loads(data.decode())
else:
def load_config(data: bytes) -> dict:
raise RuntimeError("TOML config needs Python 3.11 or newer")
print(load_config(b'name = "thumbnailer"'))go deeper
Recall the shape: import sys, then compare sys.version_info against a tuple such as (3, 12). Be ready to say why the string form of the version is the wrong thing to compare.
Explain the mechanics: it is a tuple, comparison is lexicographic, and the guard runs at import time. Show that you know a guard cannot hide syntax the parser must read first.
Demonstrate judgment about which check to reach for — feature detection for missing names, version detection for changed behaviour — and treat every guard as a branch your test matrix must actually exercise on both sides.
Own the policy: guards are debt with a maturity date. Argue for keeping them few, documented and tied to the floor you have declared, so raising that floor becomes a mechanical deletion rather than an archaeology exercise.
### What `sys.version_info` actually is `sys.version_info` is a structured value with five fields — `major`, `minor`, `micro`, `releaselevel` and `serial` — that behaves like a tuple. On CPython 3.14.7 it compares equal to `(3, 14, 7, 'final', 0)`. Because it is a tuple, Python's ordinary lexicographic tuple comparison does the right thing against a shorter tuple: `(3, 14, 7, 'final', 0) >= (3, 12)` is `True` because the first element ties and the second wins. That is the whole trick behind the idiom. The canonical guard is therefore: ```python import sys if sys.version_info >= (3, 12): from itertools import batched else: def batched(iterable, n): ... ``` Compare against a **two-element** tuple. Language features and stdlib additions land in a minor release (3.11, 3.12, 3.14) and never mid-series, so a guard on the patch number encodes a fact that does not exist. The one exception is a bug you are working around that was fixed in a specific patch release, and then you should say so in a comment, because the reader will otherwise assume you got the idiom wrong. ### Names versus syntax — the distinction that trips people A runtime guard is executed. That means it can only decide between things that are *legal to compile* on every interpreter you support. It can pick an import, choose an implementation, pass or omit a keyword argument that a newer version added. It cannot hide new syntax. CPython compiles the entire module to bytecode before executing its first statement, so a `match` statement (3.10, PEP 634), an `except*` clause (3.11, PEP 654), a PEP 695 `type` alias (3.12) or a t-string (3.14, PEP 750) inside an `if sys.version_info >= ...` block still fails to parse on an older interpreter. The only ways to isolate new syntax are to put it in a separate module that you import inside the guard, or not to write it at all until your floor moves. ### Version detection versus feature detection Asking "which interpreter is this?" and asking "is this feature available?" are different questions, and the second is usually the one you mean: ```python try: import tomllib except ImportError: tomllib = None ``` That form is correct even when a backport package supplies the name on an older interpreter, and it keeps working if the module is ever moved or removed. Use the version comparison when the difference is *behavioural* rather than about a name existing — an argument whose default changed, a return type that changed shape, a warning that became an error. Use the `try`/`except ImportError` form when a module or a name may simply be absent. ### Things that look like the idiom but are not Parsing `sys.version` is the classic mistake. `sys.version` is a human-readable banner string such as `'3.14.7 (main, ...) [Clang ...]'`; slicing or comparing it as a string gives you the wrong answer as soon as a two-digit minor arrives, which is exactly what happened to code that compared `'3.9' < '3.10'` and got `False`. Converting the version to a float — `float('%d.%d' % sys.version_info[:2]) >= 3.10` — is worse: `3.10` as a float is `3.1`, so the check silently passes on 3.1-through-3.9. Both bugs disappear the moment you compare tuples. ### Where the guard sits relative to packaging A `sys.version_info` guard is a *runtime* decision inside your code. It is not a substitute for declaring the interpreter range your distribution supports in packaging metadata, which is what stops an incompatible install from happening in the first place; nor is it a substitute for a dependency's environment marker, which switches a requirement on or off at install time. The three work at three different moments — install-time resolution, install-time requirement selection, and import time — and a well-behaved project uses all three. In practice, keep runtime guards few and local: every one of them is a branch that only one half of your test matrix exercises, and they are the first thing to delete when you raise your floor. One last convenience: static type checkers special-case comparisons against `sys.version_info` and narrow the branches accordingly, so the guarded import is understood rather than flagged as a redefinition — another reason to write the comparison in the recognised shape rather than hiding it behind a helper function.
- Why does putting a match statement inside an `if sys.version_info >= (3, 10):` block still break on 3.9?Because the guard is a runtime test and the syntax problem is a compile-time one. CPython parses and compiles the whole module before executing any of it, so an unparseable construct anywhere in the file raises SyntaxError at import, whatever branch would have run. Move the new syntax into its own module and import that module inside the guard, or wait until your supported floor makes it legal everywhere.
- When would you prefer `try: import X except ImportError:` over a version comparison?Whenever the question is whether a name exists rather than which interpreter is running. A backport can supply the module on an older release, an optional dependency may or may not be installed, and a module can move between releases; the try/except form answers all three correctly. Reserve the version comparison for behavioural differences, where the name exists on both sides but does something different.
- Is comparing `sys.version_info[:2] >= (3, 12)` any different from comparing sys.version_info directly?Functionally no, because tuple comparison already stops at the first differing element and the extra fields never get consulted against a two-element tuple. The slice is harmless but adds noise; the plain comparison is the recognised idiom, and it is the shape static type checkers look for when narrowing the two branches.
A version guard is a fork in the road you drive through; new syntax is a road sign the parser must read before the car even starts.
saying these in an interview costs you the question
- Parsing or slicing the sys.version banner string
- Comparing the version to a float like 3.10
- Believing a runtime guard hides new syntax from the parser
- Guarding on the patch number when features land per minor release
- Using a version check where try/except ImportError is the real question
- Assuming the guard replaces declaring a supported range in packaging metadata