How to determine the version of a package (dynamically) via SCM

Hey.

I’m having a bit of a hard time finding a proper solution for the following:

I’m writing a package, which needs to find out its own version, which I don’t want to have hard-coded (at least not manually, what would be okay is, if setuptools_scm writes the version to a file, bu only for builds - i.e. I don’t want it hardcoded within my git history).
setuptools and setuptools_scm are used for building and the whole thing is in git.
Oh and, while setuptools_scm is a build dependency, I would rather have it only an optional runtime dependency.
Last but not least, if my git repo has local changes (like since the the most recent tag) I’d like to have that reflected in the version.

What seems recommended is using importlib.metadata.version("my-project"), but as far as I understand this has numerous downsides:

  • At least if I look at it first (and it succeeds), I’ll never see any local changes reflected in the version - at least not since the last install (including an --editable one).
  • Consider I have an e.g. system-wide installation of a build of the package, but my code is executed from some working copy, it seems importlib.metadata.version()would simply take the system-wide installation (or wherever it finds one).

So my idea was to use setuptools_scm’s:

[tool.setuptools_scm]
version_file = "src/my_project/_version.py"

which would of course only be included when I actually build the package (including an --editable one), but then I can be sure that it’s actually “my” true version, right?

With that I’d have something like:

    try:
        from ._version import __version__
    except ModuleNotFoundError:
        pass

I ignore when it’s not found, cause that might simply be the case when I’m running from my git working area without an --editable installation.

Now how to get the dynamic determination of the version via setuptools_scm?

I thought about doing something like the following before falling back to using _version.py (it has to be that order, because if my git working dir is also an --editable, _version.py would exist):

if WITH_SETUPTOOLS_SCM:
    try:
        __version__ = setuptools_scm.get_version(root="../..", relative_to=__file__)
        print(f"from git {__version__}")
    except LookupError:
        pass

First I try importing setuptools_scm, if that succeeds (indicated by WITH_SETUPTOOLS_SCM) I try get_version.

The problems I can’t really think of a solution are the following:

  • If setuptools_scmisn’t available (at runtime), I won’t notice if it falls back to using_version.pyin an --editable install. I could perhaps try to check for a ../../.gitbut that seems brittle.
  • While very unlikely, but if for some reason the ../.. of an actual e.g. system-wide installation would contain a(nother) .git, I might pick up the wrong version.

Maybe there are even other issues I don’t see.

Any ideas how this could be done properly?

Thanks,
Philippe

I’m running into the same packaging/versioning tension: importlib.metadata reflects the installed distribution, while setuptools_scm.get_version() can reflect the checkout but adds runtime and path-detection concerns. I’m also interested in a clean pattern that keeps SCM logic build-time while making editable and source-tree behavior predictable.

I distribute an app and not a package and my release script does:

    try:
        version_file.write_text(f"__version__ = \"{version}\"")

        subprocess.run([
            sys.executable, "-m", "PyInstaller",
            str(SPEC.resolve()), "--noconfirm", "--clean",
        ], check=True)
    finally:
        version_file.unlink(missing_ok=True)

which is then consumed at runtime:

# myapp.__init__.py
try:
    from myapp._version import __version__
except ImportError:
    __version__ = "0+unknown"

If I were to ship a package I would (and be careful, this is from memory!) add the following:

# pyproject.toml
[project]
name = "mypackage"
dynamic = ["version"]

["tool.setuptools.dynamic"]
version = { attr = "mypackage._version.__version__" }
# mypackage.__init__.py
from importlib.metadata import PackageNotFoundError, version

try:
    __version__ = version("mypackage")
except PackageNotFoundError:
    __version__ = "0+unknown"

I don’t see how this solves the stated problems?
With that __init__.py you might still end up getting the version of some system-wide package, while one’s actually using a working copy + there’s no “dynamic” version detection from git/scm at all?

idk, knowledge of what I wrote solved all my single source of truth versioning issues for me. I get 0+unknown for the working copy, but it seems trivial to replace that constant with the setuptools_scm lookup. The key is to unlink _version.py when you want the dynamic lookup.

Please don’t use importlib to set __version__. If someone wants your package’s version then they can importlib.metadata.version("mypackage") themselves. If they don’t, then you’re just wasting their startup time for no reason. You’re better to not set __version__ at all than to do it this way.

Does SCM have to be the source? There are simple tools that make it easier to go the other way – put your version back in the code then let the tool keep git tags in sync.

That distinction matters here: if the committed version is authoritative, SCM lookup can stay limited to editable or source-tree builds, while installed wheels use generated metadata. It avoids making runtime Git detection a requirement for normal installs.