Skip to main content

sincewhen

PyPI - Version PyPI - Python Version

Find out which Python version added each feature your code uses.

Point sincewhen at a file and it will tell you what's in there, when each piece of it arrived, and how old a Python you could get away with running it on.

Installation

Installing with uv tool:

uv tool install sincewhen

Installing with pipx:

pipx install sincewhen

You can also install sincewhen globally with pip, but I usually recommend installing command-line tools in their own separate environment.

Usage

Give sincewhen a file to see every dated feature it uses:

$ sincewhen example.py
example.py:1  tomllib module                  3.11  2022-10-24
example.py:3  positional-only parameters (/)  3.8   2019-10-14
example.py:4  with statement                  2.5   2006-09-19

Minimum: Python 3.11, released 2022-10-24 (set by tomllib module)

The last column is the day that release shipped, so a version number reads as an age.

Only the first use of each feature is shown by default. Pass --all to see every occurrence.

The dataset reaches back to Python 0.9.1, which means a file will report things like str() and open() that have been there since 1991. Pass --since to hide them:

$ sincewhen --since 3.0 example.py

Read from standard input with -:

$ echo 'x: int = 1' | sincewhen -
<stdin>:1  variable annotation  3.6  2016-12-23

Minimum: Python 3.6, released 2016-12-23 (set by variable annotation)

Look a single feature up by name instead of analyzing code:

$ sincewhen --search walrus
walrus operator (:=) - Python 3.8 (released 2019-10-14)
  PEP: https://peps.python.org/pep-0572/
  Docs: https://docs.python.org/3/whatsnew/3.8.html

Pass --json to either mode for machine-readable output.

Library

>>> import sincewhen
>>> sincewhen.minimum_version("import tomllib")
Version(major=3, minor=11)
>>> [d.feature.name for d in sincewhen.detect("if (n := 1): pass")]
['walrus operator (:=)']
>>> sincewhen.lookup("tomllib")[0].added
Version(major=3, minor=11)

Why it needs Python 3.14

sincewhen parses code with the standard library's ast module, which can only understand syntax that the running interpreter understands. A Python 3.9 interpreter cannot parse a match statement, so it could not report one either. Requiring the newest Python is what lets sincewhen recognize the newest syntax.

The Python version you run sincewhen on has nothing to do with the versions it reports on, which reach back to Python 0.9.1.

Known limits

  • Some features are genuinely ambiguous in an AST. a | b could be a Python 3.9 dict merge, a Python 3.10 union type, or an integer bitwise-or that has worked since forever, and the AST alone cannot tell you which. Ambiguous features are left out rather than guessed at.
  • Detection is syntactic. sincewhen sees that you called something named math.isclose, not that you called the real one. Shadowed builtins are skipped, but a shadowed module attribute is not.
  • The dataset is curated and incomplete. A feature that isn't in it won't be reported, so the minimum version is a lower bound on the true answer.
  • added is the oldest release from which a feature has been available ever since, ignoring Python 3.0 and 3.1. Nobody shipped code on those two, so a gap there is not a gap anyone lived through: argparse shipped in 2.7 and again in 3.2, and is dated 2.7. A feature missing from 3.2 as well has a real gap and takes the later date.
  • Some features are older than the oldest surviving documentation, and are reported as "1.2 or earlier". Python 0.9.1 is as far back as the archives go.
  • Release dates come from python.org's downloads database back to 2.2, and from CPython's release tags before that. Python 0.9 and 1.6 have no release tag, so they show no date.
  • Searching for a module member that has no entry of its own falls back to the module it lives in, since a member cannot be older than its module.

Development

This project uses uv and just. Run just to see every available task.

$ just test     # run the test suite
$ just check    # format, lint, typecheck, and test

No setup step is needed: uv creates the virtual environment and installs dependencies on the first uv run.

If you would rather not install just, every task is a short uv command that you can run directly (check the justfile for the commands):

uv run pytest

Adding a feature

Features live in src/sincewhen/features.toml, one [[features]] table each. Give the feature an id, a human-readable name, the version that added it, a category, exactly one matcher, and the evidence for the version:

[[features]]
id = "walrus"
name = "walrus operator (:=)"
added = "3.8"
category = "syntax"
pep = 572
nodes = ["NamedExpr"]

[features.evidence]
method = "pep"
pep = 572
python_version = "3.8"
checked = "2026-07-28"

The matcher kinds are nodes (AST node class names), builtins, modules, and attributes (dotted module.name paths). Node matchers can be narrowed with requires (a node attribute that must be truthy) or check (a predicate registered in detect.py).

Documentation links are generated from added and pep, so only set docs when you have a better link than the "What's New" page.

Nobody should be typing version numbers from memory. For anything in the standard library, let the archived documentation say what the version is:

$ just fetch-docs                       # one-time, into a gitignored .cache/
$ just whenadded math.lcm               # what each source says, and whether they agree
$ just propose math.lcm math.isqrt      # entries with evidence, ready to paste
$ just verify-dataset                   # re-derive every claim in the dataset

just verify-dataset also runs in CI, so a pull request that edits a version without editing its evidence fails.

Evidence has five method values, four of which a machine can recheck:

method what it means
objects.inv the symbol is absent from one release's Sphinx inventory and present in the next
archive the same diff over the module lists and built-in function pages in the pre-Sphinx doc builds, back to the 0.9.1 LaTeX
annotation the documentation dates it itself, in an "Added in version" marker quoted in the entry
grammar the token is absent from one release's grammar and present in the next, which is what shipped rather than what a PEP intended
pep the feature's PEP carries a Python-Version header
manual a human read the archives and wrote down what they found, and why the other four do not settle it

manual is for the cases where the sources genuinely disagree, and every one of them is printed on every verify-dataset run so the override stays visible.

Releasing

Bump the version, then tag and push:

$ just bump patch
$ just release

Pushing a v* tag runs the release workflow, which publishes to PyPI with trusted publishing and creates a GitHub release.

License

sincewhen is distributed under the terms of the MIT license.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sincewhen-0.1.0.tar.gz (63.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sincewhen-0.1.0-py3-none-any.whl (59.0 kB view details)

Uploaded Python 3

File details

Details for the file sincewhen-0.1.0.tar.gz.

File metadata

  • Download URL: sincewhen-0.1.0.tar.gz
  • Upload date:
  • Size: 63.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for sincewhen-0.1.0.tar.gz
Algorithm Hash digest
SHA256 425405b960f079fc9cc037e95684ba054485a837cb68c6c2da121d5b4edd06a4
MD5 70df9b5fedd241ee1f6cc2910edb092d
BLAKE2b-256 659601f6cf3f8359681e05d3d81a45b5bab3ddb58befa4ade30775fe744c1363

See more details on using hashes here.

File details

Details for the file sincewhen-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: sincewhen-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 59.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for sincewhen-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8221fe394befc6a19a462b5a18566b1325192e5d72233d766db7f0079e5a752e
MD5 d97bd696a373a9788189d85c3d2f4292
BLAKE2b-256 44cd95c54d029d6c12f37addca57b7a9defa2f0121292ef0f283d78f8c470549

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page