Building Production (and Staging) Ready Python Applications

Author

Andres Monge

Published

January 25, 2024

Building production-ready Python applications requires careful planning and the right tools. In this article, we’ll explore how to use uv and pyenv to manage dependencies, create deployable packages, and streamline the deployment process, with a focus on modern Python packaging standards and automated versioning using uv-dynamic-versioning.

Why uv and Pyenv?

uv is an extremely fast Python package installer and resolver that’s designed to be a drop-in replacement for pip and poetry. While pyenv allows you to manage multiple Python versions seamlessly. Together, they ensure your project runs consistently across different environments with significantly improved performance.

Here’s how to get started:

  1. Install pyenv to manage Python versions.
  2. Install uv to handle dependencies and packaging.

Building and Deploying with uv

Using uv, you can create a .tar.gz pip package for your project and store it in a private pip repository. This simplifies installation and deployment in production environments.

For example, your Dockerfile could look like this:

FROM python:3.13  # or any other Python version
RUN pip install --index-url https://example.com/pypi/simple/ hello-app
WORKDIR /app
RUN hello-app-run

This approach ensures that your application is installed consistently, regardless of the environment.

Automated PEP 440 Compliant Versioning with uv-dynamic-versioning

One critical aspect of production deployments is maintaining proper versioning that follows Python’s packaging standards. The uv-dynamic-versioning plugin for uv integrates seamlessly with Git and enforces PEP 440 compliance through a custom versioning scheme.

First, install the plugin:

uv tool install uv-dynamic-versioning

Configure your pyproject.toml with the following settings:

[build-system]
requires = [
    "setuptools",
    "wheel",
] # Make sure setuptools is here for uv-dynamic-versioning

[tool.uv-dynamic-versioning]
vcs = "git"
metadata = true
strict = true
format-jinja = """
{%- if distance == 0 and branch == "main" -%}
{{major}}.{{minor}}.{{patch}}
{%- else -%}
{{major}}.{{minor + 1}}.{{patch}}.dev{{timestamp[2:8]}}+{{timestamp[8:12]}}
{%- endif -%}
"""

This configuration defines:

  • vcs = “git”: Version control system to use.
  • metadata = true: Include build metadata.
  • strict = true: Enforce strict version parsing.
  • format-jinja: Custom logic to ensure PEP 440 compliance.

How It Ensures PEP 440 Compliance

The above Jinja2 template implements a PEP 440-compliant versioning strategy:

  • On main branch with no new commits (distance == 0), it builds final releases like 1.2.3.
  • For commits on any branch (including pre-release branches), it creates development versions targeting the next minor release, such as 1.3.0.dev123456+120000.

This solves the common issue where .devN versions appear after their base version in sort order. By incrementing the minor version component ({minor + 1}), development versions correctly sort before the actual release they precede.

Example Version Sequence

To illustrate this behavior, consider the following scenario:

  1. You tag release 1.2.0
  2. Developers make new commits on the main branch
  3. A developer branches off for feature work
  4. Additional commits are made on both branches

The resulting version sequence would be:

1.2.0                     # Tagged release
1.3.0.dev234567+1200      # Commit on main after tag
1.3.0.dev234567+1201      # Another commit on main
1.3.0.dev234568+1202      # Feature branch commit (timestamp differs)

This correctly follows PEP 440’s precedence rules where development releases have lower precedence than their corresponding final release.

Manual Version Verification

You can verify the computed version at any time using:

uv-dynamic-versioning

This command will output the current version based on your Git state and configuration.

Alternative Configuration and Manual Bumping

While the automatic Git-based versioning is preferred for most cases, you might want to manually control version bumps for major releases or breaking changes. The uv-dynamic-versioning tool provides a bump command for this purpose:

# Bump major version
uv-dynamic-versioning bump major

# Bump minor version
uv-dynamic-versioning bump minor

# Bump patch version
uv-dynamic-versioning bump patch

This approach is useful when you need to explicitly control when semantic version components are incremented, such as before implementing breaking changes.

Configuring Your pyproject.toml for uv

Here is a quick example of what your pyproject.toml might look like when using uv:

pyproject.toml
[project]
name = "foo"
description = "An example application"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
    "loguru>=0.7.0",
]

[project.scripts]
packvisor-run = "src.main:main"

[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[tool.uv-dynamic-versioning]
vcs = "git"
metadata = true
strict = true
format-jinja = """
{%- if distance == 0 and branch == "main" -%}
{{major}}.{{minor}}.{{patch}}
{%- else -%}
{{major}}.{{minor + 1}}.{{patch}}.dev{{timestamp[2:8]}}+{{timestamp[8:12]}}
{%- endif -%}
"""
</details>

This configuration leverages standard setuptools with uv-dynamic-versioning for automated version management, while defining project metadata and executable scripts.

Best Practices for Production-Ready Applications

  1. Use Environment Variables: Store sensitive information like API keys and database credentials in environment variables.

  2. Automate Testing: Integrate unit tests, integration tests, and linting into your CI/CD pipeline.

  3. Monitor Logs: Use a logging library like Loguru (as shown in the Logging in Python with Loguru article) to capture and analyze runtime behavior.

  4. Optimize Docker Images: Use multi-stage builds in Docker to reduce the size of your production images.

  5. Leverage uv’s Performance: Use uv for dependency resolution and installation to speed up CI/CD pipelines and local development setups.

Conclusion

By leveraging uv, pyenv, and uv-dynamic-versioning, you can build, package, and deploy Python applications with confidence and performance. These tools simplify the process, ensure consistency across environments, and maintain PEP 440-compliant versioning automatically.

The examples and best practices provided in this article will help you create robust, production-ready applications while benefiting from the latest advancements in Python packaging.