When would you put a PEP 508 marker such as python_version < '3.11' on a dependency?
answer
- A condition attached after a semicolon
- A fixed vocabulary, no arbitrary code
- Evaluated where the install happens
- One wheel, many environments
- python_version is only major and minor
basics
~20 sWhen a requirement applies only to some environments — a backport needed below a given interpreter, or a helper needed only on one platform. The marker is evaluated by the installer in the target environment, so one artefact serves every environment.
solid answer
~40 sA PEP 508 marker is a boolean expression appended to a requirement after a semicolon, over a fixed set of environment variables: `python_version`, `python_full_version`, `sys_platform`, `platform_system`, `platform_machine`, `implementation_name` and a few others. You write `"toml-backport; python_version < '3.11'"` when a dependency exists only to fill a gap on older interpreters, and `"win-helper; sys_platform == 'win32'"` when it is platform-specific. The installer evaluates the marker against the machine it is installing into, at install time, so the same wheel is correct everywhere and the extra requirement simply disappears where it is not needed. Note the split with `requires-python`: that one gates the whole distribution and changes which *release* you get, while a marker only switches a single requirement on or off — the install proceeds either way.
code
python · 8 linesimport platform
import sys
print("python_version:", ".".join(platform.python_version_tuple()[:2]))
print("python_full_version:", platform.python_version())
print("sys_platform:", sys.platform)
print("platform_system:", platform.system())
print("platform_machine:", platform.machine())go deeper
Recognise the syntax: a requirement, a semicolon, then a condition such as python_version < '3.11'. Know that the installer decides whether the requirement applies.
Explain the vocabulary and the evaluation moment: a fixed set of variables, no arbitrary code, evaluated by the installer in the target environment so a single wheel stays correct everywhere.
Show the seam between requires-python, a dependency marker and a runtime guard, and treat every marker as a test-matrix obligation, since a wrongly-excluded dependency fails silently until import.
Own the policy on conditional dependencies: how many environment combinations the project is willing to promise, how locks capture or resolve markers, and when a backport should be dropped instead of conditionally required.
### The shape A dependency specification is a name, an optional version specifier, and an optional marker after a semicolon: ```toml [project] requires-python = ">=3.10" dependencies = [ "toml-backport>=2; python_version < '3.11'", "win-console-helper; sys_platform == 'win32'", ] ``` The marker grammar is defined by PEP 508 and is deliberately tiny: comparisons between a fixed set of environment variables and string literals, joined with `and`, `or` and parentheses. There are no function calls, no arbitrary Python, and no access to anything outside the listed variables. That restriction is the point — a marker must be evaluable by any installer, in any language, without executing project code. ### The variables worth memorising * `python_version` — major and minor only, as a string: `'3.14'`. This is the one you use for a floor or a backport switch. * `python_full_version` — the complete version, including pre-release suffixes: `'3.14.7'`, and `'3.15.0rc2'` on a release candidate. Use it when a patch level or a pre-release genuinely matters. * `sys_platform` — the value of `sys.platform`: `'linux'`, `'darwin'`, `'win32'`. * `platform_system` — the value returned by the platform module's system query: `'Linux'`, `'Darwin'`, `'Windows'`. Different spelling, same intent; pick one convention per project. * `platform_machine` — the CPU architecture, such as `'x86_64'` or `'arm64'`. * `implementation_name` — `'cpython'` on CPython, distinguishing it from other implementations. * `extra` — true only while resolving a named optional-dependency group, which is how optional extras attach conditions. Comparisons where both sides look like versions are performed with version semantics rather than string ordering, which is why `python_version >= '3.10'` behaves correctly rather than falling foul of `'3.10' < '3.9'` string ordering. Do not rely on that subtlety when you can avoid it: write the boundary you actually mean, and reach for `python_full_version` when the patch level is the thing you care about. ### Why markers exist at all: static metadata A wheel's metadata is static. It is written once at build time and read on every machine that installs it, and there is no build step on the installing side to consult. Without markers, a project whose requirements differ between platforms or interpreters would need a different artefact per environment, or a `setup.py` executed at install time — which is exactly what the ecosystem moved away from. Markers push the conditional into a declarative expression the installer evaluates in the *target* environment, so one universal wheel can honestly describe conditional requirements. That is also why the evaluation moment matters. Consider a service that renders image thumbnails, developed on macOS and deployed on Linux. A marker of `sys_platform == 'darwin'` on a development helper means the deployed image never pulls it: the marker is evaluated where the install happens, not where the wheel was built. The same is true for a lockfile produced on one platform — if the tooling resolves markers at lock time rather than recording them, the lock is only valid for the platform it was produced on, which is a well-known source of surprise. ### Markers versus `requires-python` versus a runtime guard These three are constantly confused and an interviewer will probe the seam: * `requires-python` gates the **whole distribution**. Below the floor, the installer does not install this release at all; it goes looking for an older one. * A marker on a dependency gates **one requirement**. The install proceeds regardless; the requirement is either included in the resolution or silently dropped. * A `sys.version_info` or `sys.platform` check in your code gates **one code path**, at import or call time, after everything is already installed. A typical backport arrangement uses two of them together: a marker installs the backport only below the interpreter that gained the stdlib module, and a runtime `try`/`except ImportError` picks whichever of the two is importable. The marker keeps the dependency out of modern environments; the guard keeps the code honest in both. ### Where else markers appear The same syntax works in a requirements file, in `optional-dependencies` groups, and directly on the command line — `pip install "some-backport; python_version < '3.11'"` is legal, and quoting matters because the shell would otherwise eat the semicolon. Wherever it appears, the meaning is identical: a requirement that only applies when the expression is true in the environment being installed into. ### Failure modes to name Over-marking is real. Each marker is a combination your test matrix has to cover, and a marker that is wrong is invisible — the dependency is simply absent, and the failure surfaces much later as an ImportError in the environment you did not test. Keep markers few, keep their expressions boring, and make sure the environments they discriminate between are ones you actually run tests in.
- How does a marker on a dependency differ from requires-python?requires-python gates the entire distribution: below the floor, the installer rejects that release and resolves to an older one. A marker gates a single requirement inside a release that is already considered compatible, so the install proceeds either way and the requirement is simply included or dropped. One changes which version of you gets installed; the other changes what gets installed alongside you.
- Why is python_full_version sometimes the right variable instead of python_version?python_version is only major and minor, so it cannot distinguish a patch release or a pre-release. python_full_version carries the whole value, including suffixes such as '3.15.0rc2', which is what you need when a workaround targets a specific patch-level bug or when you want to exclude pre-releases from a condition.
- What goes wrong when a lockfile resolves markers instead of recording them?The lock becomes valid only for the environment it was produced in. A lock generated on macOS that evaluated a sys_platform marker to false will omit the Linux-only requirement entirely, and the deployment fails with a missing import rather than a resolution error. Tooling that keeps the markers in the lock, or that locks per target environment, avoids this.
The marker is a conditional clause in a contract: the obligation exists only in the circumstances named, and it is the reader's circumstances that decide, not the writer's.
saying these in an interview costs you the question
- Thinking markers can call arbitrary Python at install time
- Believing markers are evaluated on the build machine
- Expecting python_version to include the patch number
- Using a marker where requires-python is the right gate
- Adding marker combinations nothing in CI ever installs
- Assuming a dropped requirement fails loudly rather than at import