Standardize .python-version file in pyproject.toml?

After migrating to uv, I noticed that it uses the .python-version file to define specific Python versions that should be targeted when creating environments. My initial reaction was that it was a little bit surprising for this one piece of information to be separated outside of pyproject.toml where other information about version constraints is stored (e.g., [project.requires-python]).

Standardizing this information into a pyproject.toml field would allow users and tooling to reference this information in a single common location and format, and updates to requires-python and python-version could be done near each other in the same file.

The list of tools that I could find that support .python-version are

.python-version files take the form:

3.11.10
3.12.9
3.13.2

Edit: The first post placed python-versions within [project], but after discussion this was moved to its own top level key.

We could map these into a key within pyproject.toml:

[project]
...
requires-python = ">=3.11"

[interpreter]
python-versions = [
    "3.11.10",
    "3.12.9",
    "3.13.2",
]

The name [interpreter] and python-versions are open for discussion.

3 Likes

Does this configuration make any sense outside of uv (or at least tools which pull in their own Python installations)? Without that, hard coded exact Python versions are almost as non-portable as hard coded Python locations.

By contrast, pyenv local creates an identical .python-version file and I’d never dream of not .gitignore-ing that file.

1 Like

FWIW, there’s a major advantage that this file is separate from pyproject.toml: when not part of git, it lets individual contributors choose which version to run their local environment (albeit you could use your tool’s environment variables). As @bwoodsend says, this file is typically in .gitignore.

The purpose of the [project] table is still heavily for the develop-a-package workflow, so it’s still favors “what is needed” vs “what is actually installed”.

It’s because it has nothing to do with project metadata but what Python versions an installation tool may/should install for some development purpose, so it doesn’t belong in [project].

That’s effectively the same tool at this point.

1 Like

What is the use case for it?

If a library declares requires-python = ">= 3.8", then why does it matter what python version anyone checking out the repository uses as long as it’s within the supported range? Seems like an arbitrary limitation to me.

And if your project isn’t a library, then this can already be achieved with requires-python = "~= 3.13.0".

Someone from Astral like @charliermarsh could probably answer better as I presume they got user requests that led them to add this feature.

But in my experience it would be similar to why even libraries might choose to check in lockfiles: to have a reproducible environment within a larger supported range of supported versions. This can be used for CI, etc where it’s helpful to have things pinned and update the pinned versions in a controlled way.

I think in the case of uv it helps select a default Python version to use when it creates a virtual environment for the project. Nothing stops you from using another Python version, but this config can provide a default for someone checking out the repo.

1 Like

I may be wrong, but doesn’t the lockfile already take requires-python under consideration? So you should be getting a reproducible environment regardless of the .python-version.

The .python-version file with pyenv is different from the uv one. With pyenv the version in the file specifies which environment to use for Python commands. The shims that are on PATH then effectively activate that environment as soon as you cd into a directory. With ordinary venv it is like:

cd some_dir
source .venv/bin/activate
python script.py

However with pyenv and .python-version you don’t need the activate step:

cd some_dir
python script.py # uses venv specified in .python-version

I found this quite convenient when I was using pyenv for everything but actually what I had in the .python-version files was always the name of a particular venv like project-3.12.git rather than a version of Python.

Since uv doesn’t have this feature I don’t bother using .python-version with uv. One reason not to share this file across machines is that you can’t really share it between uv and pyenv anyway because they don’t use it in a compatible way. If it were to go into pyproject.toml then it would make sense as a uv tool-specific configuration option but it would not make sense for the way that pyenv uses it (you don’t want to parse pyproject.toml every time you run python).

Shouldn’t it be tucked away under [tool.uv] then? [project] is reserved for interoperable metadata (and I think also metadata that has meaning to the user rather than just the developer).

You’re not going to get meaningful reproducibility across workflow managers even with the version locked. The differences between the relocatable Python installations that uv uses and pretty much any other way to get Python are far greater than the differences between micro versions of Python.

Agreed, putting it under [project] is not ideal. I was curious if others thought this was an interesting idea at all before suggesting a new top level pyproject.toml namespace for it.

The reason I was thinking of not putting it under [tool.uv] is so that other tools (pyenv, rye, setup-python, etc) could use a common configuration, like they are currently doing with .python-version. For example, you might want to share this configuration from a uv project and your CI in setup-python.

1 Like

Would anything stop tools agreeing a shared namespace for common configuration? e.g. tool.common, tool.workspace, etc.

To use [tool.$name] you have to own $name on PyPI (pep 518).

Based on feedback about [project] not being the correct place to add this, I’ve updated the strawman proposal to

[project]
...
requires-python = ">=3.11"

[interpreter]
python-versions = [
    "3.11.10",
    "3.12.9",
    "3.13.2",
]

Both key names are open for discussion (again, if the community decides this is something of value).

I still don’t see any benefit in the proposal as it currently stands.

You’re specifying multiple Python versions, and that’s entirely redundant with requires-python. What if I have Python 3.13.3? Will $tool force me to pass a flag to explicitly ignore python-versions because my patch version doesn’t match?

Today, if I have Python 3.12 on my machine, I can reasonably expect to clone any maintained Python project and start working on it.

Now, imagine if every popular project started using it and pinning arbitrary Python versions. Either my $tool would warn me about a mismatched Python version, throw an error, or download the required version on demand for every project that uses this new table. The problem would only be exacerbated if it allowed pinning to patch versions.

Nothing is stopping pyenv and asdf from reading project.requires-python and selecting an appropriate version from that. uv already reads project.requires-python and selects the latest interpreter that satisfies it, so its usage of .python-version seems to be a way of pinning the Python version to something higher than the library supports solely during development.

So, if the need seems to be using a higher minimum Python version during development (maybe you want to use something newer and fancy for, say, your documentation?), I feel like a better approach would be something like requires-python-dev = ">= 3.12". I think it’s important that its semantics are equivalent to project.requires-python, with the only change being that it doesn’t end up in your published package.

Having said that, I still don’t find it compelling enough. The Python community only really drops a Python version when upstream does, so the case for “one of my development dependencies doesn’t support one of the non-EOL Python versions” seems very rare. If you’re developing a library, you still have to run your tests with whatever your project.requires-python declares as the minimum, so you can’t really avoid it. And if you’re developing something that’s not a library, then you are already free to pin your Python in project.requires-python.

2 Likes

Agreed. The only tool I use that supports .python-version is uv, and to be perfectly honest I’ve never used that feature, and I don’t really understand the benefit of it. It certainly doesn’t seem to be a common feature - if hatch, PDM and/or Poetry also supported it, the case for standardising it would be a bit more compelling, but as it stands it seems fine as a uv-specific feature.

Also, if it was to be standardised, we’d need to define its behaviour precisely, and that may be hard given that I presume the idea would be to work the same as pyenv and the GitHub action - and we have no influence over what they do, and no guarantee that they work the way we’d want. @oscarbenjamin has already pointed out that pyenv doesn’t work like uv, so we already have a problem here.

Honestly, if pyenv did implement this, I would go looking for a way to globally and unconditionally turn it off. If that turned out to be impossible, I’d locally delete that section of the pyproject.toml for projects I absolutely need to work on and just stop contributing to the projects I don’t. I could never see implicitly installing a whole new Python installation when whatever I’ve (usually arbitrarily) set pyenv global to would do as a feature.

2 Likes

FWIW, I’m not that familiar with this file, but I do know that uv adopted this file as something that other tools were using, e.g. setup-python: https://github.com/actions/setup-python

To me, this proposal seems too underspecified to evaluate, what type of tools are supposed to read it? What does listing multiple versions mean? The versions appear to be tied CPython, how do you specify other interpreters?

1 Like

I don’t have a strong opinion on this being standardized but FYI from my perspective it’s extremely common and not uv-specific at all. It’s also supported by mise and (as mentioned before) setup-python. PDM also respects it. So you have five or six different tools (at least) that are already reading from and supporting this.

The information it captures is materially different than requires-python. requires-python just tells you the range of supported versions; meanwhile, teams use .python-version to dictate the version that should be used during development. That’s useful both for libraries and applications.

It also does not need to be a patch version – you can put 3.12 or similar to ensure that everyone is using Python 3.12, rather than anything later or earlier.

4 Likes

It would still need to be fully specified if it is to be standardised. And I suspect the idea of putting it in pyproject.toml is a mistake, in any case - there’s nothing in the idea of specifying a specific Python version to use that is restricted to projects that are intended to be packaged into a wheel (and hence have a pyproject.toml). Why would we prohibit something like a data analysis project from being able to use this feature (assuming we think it’s worth having)?

My preference is to leave it as a tool feature, though - not everything needs to be a standard. The key thing for me is that it’s something you would integrate into your project workflow, so it’s perfectly reasonable that it’s linked to your workflow tool. There’s no interoperability involved here.

1 Like

It’s not really clear what it’s supposed to do. Yeah, for a single version, it can mean “this is the python version for this environment, regardless of the range supported.” But it can also list multiple versions. Are tools supposed to interpret that as “make a venv and install all dependencies for each python version”? Typically, you’d only want to install the test dependencies on the other versions. How do users invoke each of those envs conveniently? It seems like environment specification and management is already handled by Tox, Nox, Hatch, etc. which all have their own goals and thus their own config.

1 Like