[{"content":" If you\u0026rsquo;ve ever felt overwhelmed by Python\u0026rsquo;s packaging ecosystem, you\u0026rsquo;re experiencing what the legendary XKCD comic #927 perfectly captured. The comic shows a familiar scenario: fourteen competing standards exist, someone declares \u0026ldquo;this is ridiculous, we need one universal standard,\u0026rdquo; and the final panel reveals there are now fifteen competing standards. For nearly two decades, Python packaging seemed trapped in exactly this recursive loop.\nWe transitioned from distutils to setuptools, introduced pip for installation and virtualenv for isolation, experimented with pipenv for lock files, embraced poetry for deterministic resolution, and explored pdm for standards compliance. Each tool promised to unify workflows around package installation, dependency resolution, and distribution. Each contributed valuable concepts like deterministic builds and standardized metadata, yet often created distinct, incompatible silos.\nHowever, as we survey the landscape in 2026, something unexpected has happened. Rather than merely adding another configuration format to the pile, the industry has experienced genuine consolidation. This shift wasn\u0026rsquo;t driven initially by a committee decision but by a radical change in the underlying infrastructure of the tooling itself. The narrative of 2026 is defined by the tension between mature, pure-Python tools that stabilized the ecosystem and hyper-performant, Rust-based toolchains that have redefined developer expectations for speed and efficiency.\nThe Python packaging ecosystem has undergone dramatic transformation over the past few years, and 2026 marks a pivotal moment in how we think about dependency management, distribution, and development workflows. If you\u0026rsquo;re feeling overwhelmed by the proliferation of tools like uv, poetry, pip-compile, and others, you\u0026rsquo;re not alone. This guide will help you understand the modern Python packaging landscape, choose the right tools for your projects, and build secure, efficient workflows that scale from local development to production CI/CD pipelines.\nThe Evolution of Python Packaging The journey from distutils to setuptools to pip to the modern tool ecosystem reflects Python\u0026rsquo;s growth from a scripting language to a first-class choice for everything from web applications to machine learning infrastructure. Each tool we\u0026rsquo;ll discuss emerged to solve specific problems that developers encountered in real-world projects. The introduction of PEP 517 and PEP 518 standardized build systems, the pyproject.toml file became the unified configuration format, and lock files evolved from a novel concept to an expected feature of modern workflows.\nUnderstanding where we are today requires understanding where we came from, but more importantly, understanding that the fragmentation that followed wasn\u0026rsquo;t just chaos. While initially confusing, it has actually been healthy for the ecosystem. Competition has driven innovation, and in 2026, we\u0026rsquo;re reaping the benefits of that innovation with tools that are faster, more secure, and more reliable than ever before.\nThe Modern Python Packaging Stack: An Overview Before diving into individual tools, it\u0026rsquo;s helpful to understand that Python packaging involves several distinct concerns. You need tools for dependency resolution, virtual environment management, lock file generation, package building, package distribution, and increasingly, package security. No single tool handles all of these equally well, which is why the landscape includes specialized tools for different parts of the workflow.\nThe key insight for 2026 is that you don\u0026rsquo;t need to choose just one tool. Most successful Python projects use a combination of tools, each excelling at its particular job. The art lies in understanding which combination works best for your specific needs, your team\u0026rsquo;s expertise, and your infrastructure constraints. In 2026, the fragmentation is no longer about how to define a package—thanks to the widespread adoption of pyproject.toml and PEP 621—but about which engine drives the development lifecycle.\nuv: The Rust Revolution and Performance as a Feature Released by Astral in 2024, uv has rapidly become one of the most transformative developments in Python packaging. Written in Rust, uv brings unprecedented speed to Python package installation and resolution, but more importantly, it represents a philosophical shift in how we think about developer tooling. If the early 2020s were defined by the standardization of pyproject.toml, the mid-2020s have been defined by what some call the \u0026ldquo;Rustification\u0026rdquo; of Python infrastructure. Where pip might take minutes to resolve complex dependency trees, uv often completes the same work in seconds. The performance improvements are not incremental refinements but rather represent a fundamental rethinking of how package installation should work.\nuv\u0026rsquo;s philosophy centers on speed without sacrificing correctness, and understanding the technical foundation of this speed helps explain why it has become so dominant. It implements the same resolution logic as pip but does so with aggressive caching, parallel downloads, and highly optimized algorithms. The Rust implementation allows uv to skip Python\u0026rsquo;s interpreter overhead entirely and execute dependency resolution and file operations at native speed. More importantly, uv overlaps network operations, decompression, and disk writes in a parallel pipeline, whereas pip handles each step serially. This architectural difference means that while pip waits for one operation to complete before starting the next, uv keeps all available CPU cores busy with concurrent download, extract, and install tasks.\nThe technical innovations that make uv fast deserve detailed explanation because they represent genuine advances in package management technology. On supported filesystems like APFS on macOS or Btrfs on Linux, uv uses Copy-on-Write semantics and hard links to avoid actually copying package files during installation. Instead, it maintains a global cache of all packages you\u0026rsquo;ve ever installed and creates hard links from your virtual environment to that cache. This means that if you have requests version 2.31.0 in ten different projects, the actual file data exists only once on your disk, and each project\u0026rsquo;s virtual environment simply links to that shared copy. When you create a new environment and install cached packages, the installation happens near-instantly because no actual file copying occurs, just metadata operations to create the links.\nThe performance differential has shifted uv from a niche experiment to the default choice for high-velocity engineering teams. The speed is not merely a convenience but fundamentally alters developer behavior. When an environment can be recreated in milliseconds rather than minutes, developers are more likely to test in clean environments, reducing the \u0026ldquo;it works on my machine\u0026rdquo; class of bugs that plague software development. Let\u0026rsquo;s look at real-world benchmark data to understand the magnitude of improvement:\nOperation pip + venv poetry uv Speedup Factor Cold Install (Fresh Cache) 45s 58s 1.2s ~35x Warm Install (Cached) 12s 15s 0.1s ~100x Lock File Resolution (Complex) 180s 210s 2.5s ~80x Container Build Time 3.5m 4.2m 20s ~10x Monorepo Sync (50+ packages) N/A 5m+ 15s ~20x The \u0026ldquo;Cold Install\u0026rdquo; metric is particularly relevant for CI/CD pipelines where caching strategies may not always persist between builds. The \u0026ldquo;Warm Install\u0026rdquo; metric demonstrates uv\u0026rsquo;s aggressive use of filesystem optimizations, allowing it to effectively \u0026ldquo;install\u0026rdquo; packages near-instantly by linking to a global cache rather than copying files. These aren\u0026rsquo;t benchmarks from idealized test scenarios but rather represent real-world performance across standard development tasks in 2026.\nBeyond raw speed, uv introduced a unified project management philosophy that disrupted the fragmented toolchain that existed before 2024. Prior to uv, a sophisticated Python developer\u0026rsquo;s machine was a patchwork of tools: pyenv to manage Python versions, poetry or pipenv to manage project dependencies and lock files, pipx to run global Python CLIs like black or ruff in isolated environments, and virtualenv or the venv module for manual environment creation. In 2026, uv handles this entire lifecycle, reducing the cognitive load and setup time for new contributors to Python projects.\nuv manages Python installations directly through its own managed registry of Python builds, effectively obsoleting pyenv for many users. If a project requires Python 3.13 and it\u0026rsquo;s not present on the system, uv downloads a standalone build, verifies its checksum, and uses it to bootstrap the virtual environment without any user intervention. One of uv\u0026rsquo;s most powerful features is the uvx command (or uv tool run), which allows for execution of Python tools in ephemeral, disposable environments. This replaces pipx for many use cases while being substantially faster. Running a specific version of a linter or security scanner no longer requires a global install; the tool runs in an ephemeral environment and the dependencies are cached for future use.\n# Run a tool without installing it globally - ephemeral execution uvx pycowsay \u0026#34;Hello from the ephemeral void\u0026#34; # Initialize a new project with a specific Python version # This downloads Python 3.12 if not present and creates a virtual environment uv init my-analytics-app --python 3.12 cd my-analytics-app # Add dependencies (updates pyproject.toml and uv.lock) # The resolution engine runs in parallel, utilizing all available CPU cores uv add pandas requests \u0026#34;fastapi\u0026gt;=0.110\u0026#34; uv add --dev ruff pytest # Synchronize the environment to match the lockfile exactly # This removes extraneous packages not in the lockfile uv sync # Run commands within the project\u0026#39;s environment # No need to manually activate (source .venv/bin/activate) uv run pytest # Run a one-off script with inline metadata (PEP 723) # Installs dependencies into temporary cache before execution uv run --script scripts/data_migration.py However, the \u0026ldquo;monolithization\u0026rdquo; of packaging via uv has faced valid criticism that deserves acknowledgment. The primary concern in 2026 revolves around what some call the \u0026ldquo;bus factor\u0026rdquo; risk. pip and poetry are written in Python, which means that if they break, any Python developer can debug the stack trace, understand the problem, patch the code, and potentially contribute a fix upstream. uv is written in Rust. While this grants the performance we\u0026rsquo;ve discussed, it creates a significant barrier to contribution for the vast majority of the community it serves. If a critical bug were discovered in uv\u0026rsquo;s resolver tomorrow, the number of people capable of fixing it is orders of magnitude smaller than for pip. This risk is mitigated by Astral\u0026rsquo;s corporate backing and their track record with projects like ruff, but it remains a point of contention for open-source purists who value community maintainability.\nFurthermore, architectural purists argue that uv\u0026rsquo;s aggressive bundling of features violates the Unix philosophy of small, composable tools. By combining the functionality of pip, pip-tools, pipx, poetry, pyenv, twine, and virtualenv into a single binary, uv creates a monolithic system where failure in one component could theoretically affect all others. However, the overwhelming market sentiment in 2026 suggests that developers generally prefer the convenience of a unified, \u0026ldquo;batteries-included\u0026rdquo; toolchain over modular purity, especially when that convenience comes with dramatic performance improvements.\nFor developers working on large projects with hundreds of dependencies, the time savings are transformative. Consider a typical machine learning requirements file containing packages like tensorflow, torch, scikit-learn, pandas, numpy, and matplotlib. On a local development machine with multiple cores, installing these dependencies with pip might take one minute and forty-seven seconds, while uv completes the same installation in just thirty-five seconds. That represents a sixty-six percent improvement in wall clock time, and more importantly, it means the difference between staying in flow and context-switching to other tasks while waiting for installations to complete.\nThe tool excels particularly in continuous integration environments, but there\u0026rsquo;s an important caveat that becomes clear when you understand the source of uv\u0026rsquo;s speed advantage. Much of uv\u0026rsquo;s performance gain comes from full CPU utilization through concurrent operations. On a modern development machine with four, eight, or more cores, this parallelism delivers dramatic improvements. However, CI environments often run on virtual machines with limited CPU allocation. A typical GitHub Actions runner or AWS CodeBuild instance might have only one or two virtual CPUs assigned to it.\nTo understand the real-world impact, consider benchmark data from testing both pip and uv in Docker containers with CPU limits that simulate CI environments. When installing the same machine learning dependencies with a single CPU core allocated (using Docker\u0026rsquo;s --cpus 1 flag), pip takes about one minute and forty-three seconds, while uv completes the installation in one minute and thirty-five seconds. That\u0026rsquo;s still an improvement of about eight seconds, but it\u0026rsquo;s nowhere near the seventy-two second improvement seen on unrestricted hardware. The benefit shrinks because you\u0026rsquo;ve removed the primary advantage that makes uv faster: its ability to saturate multiple CPU cores with parallel operations.\nThis doesn\u0026rsquo;t mean uv isn\u0026rsquo;t valuable in CI environments. Eight seconds saved per build still compounds over hundreds or thousands of builds. However, it does mean that teams expecting to replicate their local development speed improvements in CI may be disappointed if they\u0026rsquo;re using standard, minimal CI runner configurations. The solution, which we\u0026rsquo;ll discuss more in the section on private package repositories, is to address the other bottleneck: network latency and PyPI\u0026rsquo;s response times. Even with a single CPU core, uv can deliver substantial improvements when paired with a faster package index that reduces the time spent waiting for package metadata and downloads.\nThe ideal use case for uv is as a comprehensive solution for modern projects that primarily pull from PyPI or standard private repositories. It works beautifully with requirements.txt files, natively generates lock files, and provides a unified developer experience from project initialization through deployment. For teams using a private PyPI server like RepoForge.io, uv\u0026rsquo;s speed benefits extend to internal packages as well, making development iteration substantially faster. The combination of uv\u0026rsquo;s parallel execution with a low-latency private Python package registry can deliver the same dramatic improvements in CI that you experience locally, even on CPU-constrained runners.\npoetry: The All-in-One Developer Experience poetry represents a different philosophy from uv. Rather than focusing on one piece of the packaging puzzle, poetry aims to be a comprehensive solution for Python project management. It handles dependency management, virtual environments, package building, and publishing all through a single, cohesive interface. For developers who appreciate opinionated tools that make decisions for you, poetry offers an attractive workflow that feels modern and well-integrated from end to end.\nThe centerpiece of poetry is the pyproject.toml file, which serves as the single source of truth for your project. This file defines your dependencies, build system, metadata, and tool configuration in a standardized format that\u0026rsquo;s becoming increasingly common across the Python ecosystem. poetry then generates a poetry.lock file that pins exact versions of all dependencies and their transitive dependencies, ensuring that everyone working on the project has identical environments. This lock file approach eliminates the classic \u0026ldquo;works on my machine\u0026rdquo; problem by guaranteeing reproducible builds across development, staging, and production environments.\nFor developers coming from Node.js and npm, poetry feels familiar and intuitive. The workflow of adding dependencies, installing packages, and building distributions maps naturally to npm\u0026rsquo;s commands, but with Python-specific enhancements. This familiarity has made poetry popular among developers who work across multiple languages and appreciate consistent workflows regardless of the underlying ecosystem.\nLet\u0026rsquo;s look at how you actually use poetry in practice. When starting a new project, poetry creates the project structure and configuration for you with sensible defaults. You can then add dependencies interactively, and poetry handles version resolution and lock file updates automatically.\n# Create a new poetry project poetry new my-awesome-project cd my-awesome-project # Add a dependency - poetry resolves versions and updates the lock file poetry add requests # Add a development dependency poetry add --group dev pytest # Install all dependencies from the lock file poetry install # Run a command within the virtual environment poetry run python my_script.py # Build your package for distribution poetry build # Publish to PyPI or a private repository poetry publish --repository my-private-repo poetry\u0026rsquo;s dependency resolver is sophisticated and generally produces correct results even with complex dependency graphs. It understands semantic versioning, can navigate tricky dependency constraints, and provides clear error messages when conflicts arise. When you add a new package, poetry figures out if it\u0026rsquo;s compatible with your existing dependencies and updates your lock file accordingly. If there\u0026rsquo;s a conflict, poetry explains what\u0026rsquo;s incompatible and why, helping you understand the constraint problem rather than just failing mysteriously.\nThe all-in-one nature of poetry is both its greatest strength and a potential limitation depending on your needs. If you\u0026rsquo;re starting a new project and want to get up and running quickly with modern Python packaging practices, poetry provides an excellent experience. A single tool handles everything you need, from project initialization through dependency management to package publishing. The learning curve is manageable because you\u0026rsquo;re learning one tool\u0026rsquo;s paradigm rather than juggling multiple tools with different interfaces and assumptions.\nHowever, if you have specific requirements for parts of your workflow, poetry\u0026rsquo;s integrated approach can feel constraining. Perhaps you need pip-compile\u0026rsquo;s features for generating constraints files for complex multi-environment setups, or you prefer a different virtual environment manager, or you want to use a specific build backend that poetry doesn\u0026rsquo;t support. In these cases, poetry\u0026rsquo;s opinion about how things should work might conflict with your requirements. The tool makes strong assumptions about workflow, and while those assumptions work well for many projects, they\u0026rsquo;re not universally applicable.\nPerformance is another consideration worth understanding. While poetry has improved significantly over the years through optimization work and better dependency resolution algorithms, it\u0026rsquo;s still slower than specialized tools like uv for package installation. The resolver has to do more work because it\u0026rsquo;s providing additional features like semantic version understanding and comprehensive conflict detection. For many projects, especially smaller to medium-sized applications, this trade-off makes perfect sense. You sacrifice some raw installation speed in exchange for a more curated, integrated experience that catches problems early and provides better error messages when things go wrong.\npoetry shines particularly in application development rather than library development. If you\u0026rsquo;re building a web application with Django or FastAPI, a data pipeline with Airflow, or an internal tool for your organization, poetry provides an excellent end-to-end workflow. The lock file ensures your production deployments match your development environment exactly. The integrated virtual environment management means you don\u0026rsquo;t need to remember to activate environments manually. The build and publish commands are right there when you need them, using the same tool and configuration you\u0026rsquo;ve been using throughout development.\npip-compile and pip-tools: The Pragmatic Middle Ground pip-tools, and specifically its pip-compile command, represents a pragmatic approach to dependency management that appeals to developers who want reproducible builds without adopting an entirely new workflow. Rather than replacing your entire packaging stack, pip-tools augments the standard pip and requirements.txt approach with lock file functionality. This incremental improvement strategy makes pip-tools particularly attractive for teams with existing projects who want to adopt modern practices without a complete rewrite of their build and deployment pipelines.\nThe core concept is simple but powerful, and understanding it helps clarify why pip-tools has remained popular even as newer, flashier tools have emerged. You maintain a requirements.in file with your direct dependencies, potentially with loose version constraints that express what you actually care about. You then run pip-compile to generate a requirements.txt file with all dependencies pinned to specific versions, including all transitive dependencies that your direct dependencies rely on. This gives you reproducible environments while keeping your dependency declarations clean and maintainable. Your requirements.in file says \u0026ldquo;I need Django and Redis,\u0026rdquo; while your requirements.txt file says \u0026ldquo;I need Django 4.2.7, asgiref 3.7.2, sqlparse 0.4.4, redis 5.0.1, and async-timeout 4.0.3,\u0026rdquo; capturing the entire dependency tree.\nThis approach has several advantages that become clear when you start using it in real projects. First, it\u0026rsquo;s minimal and focused. pip-tools does one thing well rather than trying to be everything to everyone. This focused scope means it\u0026rsquo;s easy to understand, easy to debug when problems occur, and easy to integrate into existing workflows. Second, it integrates seamlessly with existing Python tooling because you\u0026rsquo;re still using pip for installation. Any workflow or tool that works with pip works with pip-tools. Your deployment scripts don\u0026rsquo;t need to change. Your CI configuration doesn\u0026rsquo;t need to change. You simply add one compilation step that generates the lock file from your input file.\nThird, pip-tools is transparent in a way that some integrated tools are not. The generated requirements.txt file is human-readable, making it easy to understand exactly what\u0026rsquo;s installed and why. If you\u0026rsquo;re curious about where a particular package came from, you can trace it back through the comments that pip-compile adds to the requirements.txt file. These comments show which top-level requirement pulled in each transitive dependency, creating an audit trail that helps you understand your dependency graph.\nHere\u0026rsquo;s how you use pip-compile in practice. The workflow centers around maintaining .in files for your dependencies and compiling them into .txt files that get checked into version control and used for actual installation.\n# Install pip-tools pip install pip-tools # Create a requirements.in file with your direct dependencies echo \u0026#34;django\u0026gt;=4.2\u0026#34; \u0026gt; requirements.in echo \u0026#34;redis\u0026gt;=5.0\u0026#34; \u0026gt;\u0026gt; requirements.in echo \u0026#34;celery\u0026gt;=5.3\u0026#34; \u0026gt;\u0026gt; requirements.in # Compile into a fully pinned requirements.txt pip-compile requirements.in # Install the pinned dependencies pip install -r requirements.txt # Update dependencies to latest compatible versions pip-compile --upgrade requirements.in # Update just one dependency pip-compile --upgrade-package django requirements.in pip-compile excels at generating different requirements files for different purposes, and this capability becomes important as your project grows in complexity. You might have requirements.in for production dependencies, dev-requirements.in for development tools like pytest and black, and docs-requirements.in for documentation generation with Sphinx. You can use constraints files to ensure consistency across these different environments, guaranteeing that when a transitive dependency appears in multiple compiled files, it has the same version everywhere. This flexibility makes pip-tools popular in organizations with complex deployment requirements where different services or environments need different but overlapping sets of dependencies.\nThe tool also integrates naturally with private package repositories, which becomes increasingly important as your organization grows and you need to distribute internal packages. If you\u0026rsquo;re using RepoForge.io to host internal packages, pip-compile respects your pip configuration and generates lock files that include your private packages exactly like PyPI packages. The workflow remains consistent whether you\u0026rsquo;re pulling from public or private sources. You can even use pip-compile\u0026rsquo;s --index-url and --extra-index-url options to specify your private repository explicitly.\n# Compile with a private repository pip-compile --index-url https://api.repoforge.io/your-hash/ requirements.in # Or use extra index to search both PyPI and your private repo pip-compile --extra-index-url https://api.repoforge.io/your-hash/ requirements.in One limitation of pip-tools that\u0026rsquo;s worth understanding is that it doesn\u0026rsquo;t handle virtual environment management. You need to use venv, virtualenv, or another environment manager separately. For some developers, this separation is actually preferable because it keeps concerns separate and lets you use your preferred environment management approach. For others, the integrated experience of poetry or hatch is more appealing. Neither approach is inherently better, but understanding the trade-off helps you choose what fits your workflow.\npip-compile also doesn\u0026rsquo;t have a concept of semantic versioning resolution the way poetry does. It uses pip\u0026rsquo;s resolver under the hood, which means it gets the same correct results but doesn\u0026rsquo;t provide the same level of feedback about why certain versions were chosen or what constraints are in conflict. When a resolution fails, you get pip\u0026rsquo;s error message, which can sometimes be cryptic for complex dependency graphs. However, for most real-world projects, this is rarely a problem, and the simplicity of pip-tools\u0026rsquo; approach outweighs the occasional difficulty in understanding resolution conflicts.\nPackage Distribution and Security: Why You Need Private Repositories Understanding the tools that install and manage packages is only half the story. The other critical piece is understanding how packages get distributed and why the default approach of publishing everything to public PyPI creates security risks that many organizations don\u0026rsquo;t fully appreciate. Package distribution is where abstract concerns about dependency management become concrete security and business continuity issues that can affect your entire organization.\nThe public Python Package Index has served the Python community extraordinarily well for many years. It\u0026rsquo;s free, it\u0026rsquo;s fast enough for most purposes, and it has excellent uptime. However, PyPI\u0026rsquo;s openness, which is one of its greatest strengths for the community, also creates vulnerabilities that become problematic as your projects move from hobby experiments to production systems that handle real user data and business-critical operations.\nThe most insidious security threat facing Python developers today is typosquatting, sometimes called dependency confusion attacks. The attack works because of a simple human vulnerability: developers make typos. An attacker registers a package with a name that\u0026rsquo;s one character different from a popular package, like \u0026ldquo;requestes\u0026rdquo; instead of \u0026ldquo;requests\u0026rdquo; or \u0026ldquo;beatifulsoup4\u0026rdquo; instead of \u0026ldquo;beautifulsoup4\u0026rdquo;. They might also register \u0026ldquo;numpy-utils\u0026rdquo; when the real package is just \u0026ldquo;numpy\u0026rdquo;, exploiting developers\u0026rsquo; assumptions about how packages are named. The malicious package often includes the real package as a dependency, so the installation seems to work correctly. However, it also includes code that exfiltrates environment variables, steals AWS credentials, opens reverse shells, or installs cryptocurrency miners. By the time you notice something is wrong, the damage is done.\nWhat makes typosquatting particularly dangerous is that it can affect you even if you never make a typo yourself. If one of your dependencies gets typosquatted, and you install it through a transitive dependency chain, you might pull in the malicious package without ever having typed its name. Your requirements file says \u0026ldquo;install framework-x\u0026rdquo;, framework-x depends on \u0026ldquo;utility-y\u0026rdquo;, but utility-y\u0026rsquo;s maintainer made a typo in their setup.py and accidentally specified \u0026ldquo;requestes\u0026rdquo; instead of \u0026ldquo;requests\u0026rdquo;. Now your entire infrastructure is potentially compromised because of someone else\u0026rsquo;s typo three levels deep in your dependency tree.\nThe scale of this problem is sobering. Security researchers regularly find typosquatted packages on PyPI, and while the PyPI team removes them when discovered, there\u0026rsquo;s always a window of time between when a malicious package is uploaded and when it\u0026rsquo;s detected and removed. During that window, thousands of developers might install it. The attacks are becoming more sophisticated too. Early typosquatting attempts were crude and easy to detect, but modern attacks include actual functionality that makes them harder to identify as malicious. Some attackers even contribute legitimate features to make their packages seem valuable while hiding malicious code in infrequently executed code paths.\nEven beyond intentional attacks, public PyPI creates operational risks. A package you depend on might be removed by its author, either intentionally or because of a dispute with PyPI\u0026rsquo;s moderation team. A maintainer might abandon a project, and the package namespace might get taken over by someone with different intentions. The package might be updated in a way that breaks your application, and if you don\u0026rsquo;t have version pinning in place, your next deployment could suddenly fail. These scenarios, while less dramatic than security attacks, can still cause significant business disruption.\nThe solution to these problems is to use private package repositories for your internal code and, increasingly, for all your dependencies including those originally from PyPI. A private repository gives you complete control over what packages are available to your developers and what versions of those packages they can install. This control enables several important security practices that are difficult or impossible with public PyPI alone.\nFirst, you can curate your allowed packages. Rather than giving developers access to all two hundred thousand packages on PyPI, many of which are abandonware or outright malicious, you explicitly choose which packages your organization trusts. This curation process might seem like overhead, but it\u0026rsquo;s far less overhead than responding to a security incident caused by a typosquatted package. When a developer needs a new package, they request it, your security team reviews it, and once approved, it goes into your private repository where all developers can access it. This creates a firewall between the Wild West of public PyPI and your production systems.\nSecond, you can scan packages for vulnerabilities before they reach your developers. When you pull a package from PyPI into your private repository, you can run automated security scans that check for known CVEs in the code, inspect the package contents for suspicious patterns, and verify that the package actually does what it claims to do. If a vulnerability is discovered in a package you\u0026rsquo;re already using, you can block the vulnerable versions in your private repository, preventing new projects from using them while you plan migration strategies for existing uses.\nThird, you can ensure business continuity even if PyPI has issues. If PyPI goes down, has performance problems, or starts rate-limiting your CI pipeline because of heavy use, your private repository continues serving packages normally. You\u0026rsquo;ve essentially created a local cache of all the packages you need, and that cache is under your control. This independence becomes particularly important during incident response. When you\u0026rsquo;re trying to roll back to a working version of your application because of a production issue, the last thing you want is to discover that PyPI is slow or a critical package has been deleted.\nFourth, and perhaps most importantly, a private repository gives you a secure place to host internal packages that shouldn\u0026rsquo;t be public. Every organization eventually builds shared libraries, internal tools, or business logic that needs to be packaged and distributed across multiple projects. Publishing these to public PyPI is obviously not an option because it would expose your internal code to the world. A private repository gives you a way to distribute internal packages using the same workflows and tools you use for public packages. Your developers install internal packages the same way they install public ones, and your CI systems don\u0026rsquo;t need special cases for internal versus external dependencies.\nThis is precisely where RepoForge.io becomes an essential part of your Python infrastructure. Rather than setting up and maintaining your own package index server, with all the associated infrastructure complexity, security patching, backup management, and operational overhead that entails, RepoForge provides a hosted private Python package registry — a fully managed private PyPI server that works seamlessly with all the tools we\u0026rsquo;ve discussed. You get the security benefits of a private repository without the operational burden of running one yourself.\nThe workflow is straightforward and designed to feel natural to anyone familiar with Python packaging. You configure your development machines and CI systems to use your RepoForge URL as a package index. This can be your only index, meaning all packages must go through RepoForge first, or it can be an additional index that supplements PyPI. When you want to use a public package, you publish it to your RepoForge repository once, either manually or through an automated approval process. From that point forward, all your developers and CI systems install that package from RepoForge. When you create internal packages, you publish them to RepoForge using the same tools you\u0026rsquo;d use for PyPI, like twine or poetry\u0026rsquo;s publish command.\n# Configure pip to use RepoForge pip config set global.index-url https://api.repoforge.io/your-hash/ # Or use it alongside PyPI pip config set global.extra-index-url https://api.repoforge.io/your-hash/ # Install packages normally - they come from RepoForge pip install requests django # Publish your internal package to RepoForge twine upload --repository-url https://api.repoforge.io/your-hash/ dist/* The performance characteristics of RepoForge matter as much as its security benefits, especially when we consider the CI/CD discussion from earlier. Remember how uv\u0026rsquo;s performance advantage diminishes in CPU-constrained CI environments because its parallelism can\u0026rsquo;t compensate for single-core limitations? The other major bottleneck in those environments is network latency and package index response time. PyPI is fast but not instantaneous, and when you\u0026rsquo;re installing dozens of packages, those individual latencies compound into significant total installation time.\nRepoForge addresses this by providing extremely low-latency API responses and aggressive CDN caching. When your CI runner requests package metadata or downloads a wheel file, RepoForge serves it from edge locations close to your infrastructure, reducing round-trip times dramatically. In benchmarked testing with that same machine learning requirements file we discussed earlier, installing from RepoForge instead of PyPI in a single-CPU Docker container reduced installation time from one minute forty-three seconds to one minute twenty-seven seconds, even when using standard pip. That\u0026rsquo;s a sixteen-second improvement just from reducing network latency. When you combine RepoForge with uv, installation time drops to forty-eight seconds, representing a fifty-five percent improvement over the baseline pip-from-PyPI approach. These improvements compound across every CI build, every developer environment setup, and every deployment, ultimately translating to faster development cycles, lower CI costs, and developers spending less time waiting for dependencies to install.\nThe security and performance benefits we\u0026rsquo;ve discussed make private repositories like RepoForge essential infrastructure for professional Python development. The question is not whether you\u0026rsquo;ll eventually need a private repository, but rather how much pain you\u0026rsquo;ll endure from security incidents, slow CI pipelines, and fragile PyPI dependencies before you implement one. Starting with private repositories early in your project\u0026rsquo;s lifecycle establishes secure patterns from the beginning and avoids the difficult migration process of retrofitting security into an existing system built around direct PyPI access.\npip: The Foundational Standard Despite the proliferation of new tools with impressive performance characteristics and integrated workflows, pip remains the foundation of Python packaging. Understanding pip is essential because nearly every other tool in the ecosystem either builds on pip, wraps pip, or at minimum needs to interoperate with pip\u0026rsquo;s conventions and behaviors. pip is what actually installs packages at the lowest level, even when you\u0026rsquo;re using higher-level tools that invoke it behind the scenes and hide that implementation detail from you.\npip\u0026rsquo;s role in 2026 has evolved from being the primary tool that developers interact with daily to being more of a foundational layer that provides core functionality for the rest of the ecosystem. It\u0026rsquo;s less common to use pip directly for dependency management in production applications where lock files and reproducible builds are essential. Instead, pip serves as the installation engine that other tools leverage when they need to actually put packages into Python environments. However, for quick experiments, small scripts, exploratory data analysis, or system-level Python installations where you\u0026rsquo;re just getting something working quickly without worrying about reproducibility, pip remains perfectly adequate and familiar.\nThe pip resolver, which was completely rewritten and released in 2020 after years of development work, provides correct dependency resolution according to Python\u0026rsquo;s packaging standards. While it\u0026rsquo;s slower than newer tools like uv that optimize for speed through parallelism and compiled code, pip is reliable and handles edge cases well because of its maturity and conservative implementation. For projects with complex dependency graphs, unusual version constraints, or packages that do strange things during installation, pip\u0026rsquo;s conservative approach sometimes produces better results than faster alternatives that optimize for the common case but might stumble on edge cases.\nHere\u0026rsquo;s how you use pip for basic package management, which establishes the baseline that other tools improve upon:\n# Install a package pip install requests # Install with version constraints pip install \u0026#39;django\u0026gt;=4.0,\u0026lt;5.0\u0026#39; # Install from a requirements file pip install -r requirements.txt # Install from a specific index pip install --index-url https://api.repoforge.io/your-hash/ mypackage # Upgrade a package to the latest version pip install --upgrade requests # Uninstall a package pip uninstall requests # Show installed packages pip list # Show package information pip show django pip also serves as the reference implementation for Python packaging standards, which gives it a special status in the ecosystem beyond just being another tool. When questions arise about how something should work, how a particular PEP should be interpreted, or how packages should behave in edge cases, pip\u0026rsquo;s behavior often defines the answer. This makes pip crucial for package authors who want to ensure their packages work correctly across different tools and environments. If your package works with pip, it will almost certainly work with everything else. The reverse is not always true.\nsetuptools: The Build System Foundation setuptools holds a special and historically important place in Python packaging. For many years, it was the standard way to define how Python packages should be built and distributed. While new build systems have emerged in recent years offering different approaches and capabilities, setuptools remains widely used and deeply integrated into the ecosystem. Understanding setuptools helps you understand the foundations that modern tools are built upon, and it remains directly relevant if you\u0026rsquo;re maintaining existing packages or need fine-grained control over the build process.\nThe key thing to understand about setuptools in 2026 is that it\u0026rsquo;s primarily a build backend rather than a complete packaging solution that handles every aspect of your workflow. When you define your project in pyproject.toml with build-system.requires including setuptools, you\u0026rsquo;re specifying that setuptools should handle the process of turning your source code into distributable wheels and source distributions that can be uploaded to package indexes. This build backend role is distinct from dependency management, installation, or environment management, which are handled by other tools in your stack.\nsetuptools is mature in the best sense of that word. It\u0026rsquo;s been around long enough to have encountered and solved many edge cases that newer build systems might miss or handle incorrectly. If you\u0026rsquo;re working with C extensions that need to be compiled, with data files that need to be included in your distribution, with namespace packages that have tricky import requirements, or with complex package structures that don\u0026rsquo;t follow simple conventions, setuptools has likely encountered your problem before and provides mechanisms to handle it. The documentation is extensive if sometimes dense, and the community knowledge around setuptools is vast because of its long history.\nHowever, setuptools can be verbose and sometimes confusing, particularly if you\u0026rsquo;re looking at older projects that use setup.py files. The setup.py approach, while still supported for backwards compatibility, has largely given way to declarative configuration in pyproject.toml or setup.cfg files. This shift toward declarative configuration has made setuptools more approachable for new projects, but the legacy of setup.py means there\u0026rsquo;s a lot of outdated information scattered across the internet that can confuse developers trying to learn modern setuptools practices.\nHere\u0026rsquo;s how setuptools is typically used in modern Python projects through pyproject.toml configuration:\n# pyproject.toml - Modern setuptools configuration [build-system] requires = [\u0026#34;setuptools\u0026gt;=61.0\u0026#34;] build-backend = \u0026#34;setuptools.build_meta\u0026#34; [project] name = \u0026#34;mypackage\u0026#34; version = \u0026#34;1.0.0\u0026#34; description = \u0026#34;A sample Python package\u0026#34; authors = [{name = \u0026#34;Your Name\u0026#34;, email = \u0026#34;you@example.com\u0026#34;}] dependencies = [ \u0026#34;requests\u0026gt;=2.28.0\u0026#34;, \u0026#34;click\u0026gt;=8.0.0\u0026#34; ] [project.optional-dependencies] dev = [\u0026#34;pytest\u0026gt;=7.0\u0026#34;, \u0026#34;black\u0026gt;=23.0\u0026#34;] # Build your package using setuptools python -m build # This creates wheel and source distributions in dist/ # dist/mypackage-1.0.0-py3-none-any.whl # dist/mypackage-1.0.0.tar.gz For library authors and package maintainers, setuptools remains a solid choice when you need a mature, well-understood build backend. It\u0026rsquo;s compatible with every tool in the ecosystem, produces standard-compliant packages that work everywhere, and gives you fine-grained control over the build process when you need it. For application developers who mostly consume packages rather than publishing them, you may never interact with setuptools directly, as your dependency management tool handles any setuptools interactions behind the scenes.\ntwine: The Secure Distribution Tool twine serves a focused and important purpose in the Python packaging ecosystem. It uploads packages to package indexes like PyPI or private repositories like RepoForge.io. While this might seem like a small role compared to the comprehensive tools we\u0026rsquo;ve discussed that handle multiple aspects of packaging, twine does its job well and securely, which matters enormously when you\u0026rsquo;re publishing code that other developers will depend on or that represents your organization\u0026rsquo;s intellectual property.\nThe primary advantage of twine over alternative approaches is that it uses HTTPS exclusively for uploads and validates TLS certificates, ensuring that your package uploads are secure and can\u0026rsquo;t be intercepted or tampered with during transit. This security focus matters particularly when you\u0026rsquo;re publishing open-source packages to PyPI, as it protects against various attack vectors during the upload process. For private packages published to internal repositories, this same security ensures that your proprietary code is protected as it moves from your build system to your package index.\ntwine\u0026rsquo;s workflow is straightforward and intentionally minimal. After building your package with your chosen build system, whether that\u0026rsquo;s setuptools, hatchling, or another build backend, you run twine upload to publish to your target repository. The tool handles authentication, validates that your package meets basic requirements, and provides clear feedback if something goes wrong. This simplicity makes twine reliable for both manual releases where a human is running the upload command and automated CI/CD pipelines where the upload happens as part of your deployment process.\nHere\u0026rsquo;s how you use twine in practice for both public and private package distribution:\n# Install twine pip install twine # Build your package first (using setuptools, hatch, etc.) python -m build # Upload to PyPI twine upload dist/* # Upload to a private repository like RepoForge twine upload --repository-url https://api.repoforge.io/your-hash/ dist/* # Configure repository in ~/.pypirc for convenience cat \u0026gt; ~/.pypirc \u0026lt;\u0026lt; EOF [distutils] index-servers = pypi repoforge [pypi] username = __token__ password = pypi-your-token-here [repoforge] repository = https://api.repoforge.io/your-hash/ username = token password = your-repoforge-token EOF # Now you can upload using the configured name twine upload --repository repoforge dist/* # Check your package before uploading to catch issues early twine check dist/* For organizations using private package repositories, twine is often the bridge between your build system and your package index, regardless of which build system you chose. Whether you\u0026rsquo;re building with setuptools, hatch, or poetry\u0026rsquo;s build command, twine provides a consistent interface for the upload step. The tool respects the standard .pypirc configuration file for storing repository credentials, making it easy to manage multiple deployment targets without embedding credentials in your scripts or CI configuration.\ntwine doesn\u0026rsquo;t try to do more than it needs to, and this focused approach makes it robust and reliable. It doesn\u0026rsquo;t build packages, manage dependencies, or handle environments. It simply uploads packages that you\u0026rsquo;ve already built to indexes that you specify. When you need to publish a package, twine does exactly what you expect without surprises or unnecessary complexity. This reliability is why twine remains the standard tool for package distribution even as other parts of the packaging ecosystem evolve and change.\nhatch: The Modern, Standards-Based Alternative hatch represents the newer generation of Python packaging tools with a focus on standards compliance and flexibility. Created by a PyPA member, hatch is designed around current Python packaging standards rather than creating proprietary formats or workflows. Like poetry, it provides an integrated experience for dependency management, environment management, building, and publishing, but it does so while maintaining compatibility with standard Python packaging conventions.\nWhat makes hatch particularly interesting is its excellent support for managing multiple environments. You can define different environments for different Python versions, different dependency sets, or different purposes like testing, documentation, or development. For library maintainers who need to test against multiple Python versions and dependency combinations, this capability makes hatch especially valuable. The tool\u0026rsquo;s approach to versioning is also noteworthy, providing built-in version bumping and dynamic versioning from VCS tags, which streamlines release management. For projects that follow semantic versioning, hatch reduces the manual overhead of managing version numbers across releases.\nPDM: Standards-Compliant Innovation PDM takes yet another approach to Python packaging by positioning itself as a modern tool that strictly adheres to Python packaging standards while providing excellent performance. Like poetry and hatch, it generates lock files for reproducible environments, but it does so while maintaining close compatibility with packaging standards rather than introducing proprietary formats. This standards-focused approach means PDM projects are generally more interoperable with other tools and less likely to encounter compatibility issues with the broader ecosystem.\nWhat makes PDM particularly interesting in 2026 is its hybrid capability. You can configure PDM to use uv as its installation backend by setting pdm config use_uv true, combining PDM\u0026rsquo;s flexible, standards-compliant CLI and plugin system with uv\u0026rsquo;s installation speed. This \u0026ldquo;hybrid\u0026rdquo; approach appeals to developers who want the best of both worlds—Pythonic configuration with Rust-based performance. The tool provides an interesting middle ground between the full integration of poetry and the focused approach of pip-tools, making it worth considering if you want modern features while ensuring your projects remain portable and standards-compliant.\nModern Python Packaging Standards: The Foundation of Interoperability The chaos of \u0026ldquo;14 competing standards\u0026rdquo; from the XKCD comic has been substantially mitigated not by eliminating choices but by standardizing the interfaces between tools. In 2026, several key Python Enhancement Proposals (PEPs) have created a foundation of interoperability that allows different tools to coexist productively. Understanding these standards helps you appreciate why the modern ecosystem feels less fragmented than it did just a few years ago, even though more tools exist than ever before.\nPEP 621: The Unifying Configuration Standard PEP 621 standardizes how project metadata—name, version, dependencies, authors, and other critical information—is defined in pyproject.toml files. This standard is now supported by all major tools, which means you can define dependencies in standard TOML format and theoretically switch between uv, pdm, and hatch without rewriting your dependency lists. This interoperability represents a significant achievement for the Python Packaging Authority and has dramatically reduced the friction of moving between tools or maintaining projects that different team members work on with different tool preferences.\nHere\u0026rsquo;s what a standard PEP 621 configuration looks like, compatible with uv, pdm, and hatch:\n# A standard PEP 621 configuration [project] name = \u0026#34;enterprise-api\u0026#34; version = \u0026#34;2.1.0\u0026#34; description = \u0026#34;Core API service for data ingestion\u0026#34; requires-python = \u0026#34;\u0026gt;=3.11\u0026#34; authors = [ { name = \u0026#34;Platform Engineering\u0026#34;, email = \u0026#34;engineering@example.com\u0026#34; } ] dependencies = [ \u0026#34;fastapi\u0026gt;=0.109.0\u0026#34;, \u0026#34;sqlalchemy[asyncio]\u0026gt;=2.0.0\u0026#34;, \u0026#34;pydantic-settings\u0026gt;=2.1.0\u0026#34; ] [project.optional-dependencies] dev = [\u0026#34;pytest\u0026gt;=7.0\u0026#34;, \u0026#34;black\u0026gt;=23.0\u0026#34;, \u0026#34;ruff\u0026gt;=0.1.0\u0026#34;] docs = [\u0026#34;mkdocs\u0026gt;=1.5\u0026#34;, \u0026#34;mkdocs-material\u0026gt;=9.0\u0026#34;] [build-system] requires = [\u0026#34;hatchling\u0026#34;] build-backend = \u0026#34;hatchling.build\u0026#34; The beauty of this standard is that it decouples your project\u0026rsquo;s metadata from your choice of tooling. Your project definition becomes portable, and you\u0026rsquo;re not locked into a particular tool\u0026rsquo;s ecosystem simply because you\u0026rsquo;ve committed to a configuration format.\nPEP 723: Inline Script Metadata Revolution One of the breakthrough features of 2026 is the widespread adoption of PEP 723, which allows dependencies to be declared directly inside a Python script using a specially formatted comment block. This solves an age-old problem in Python: sharing a single-file script that requires third-party packages. Previously, sharing such a script required also sending a requirements.txt file and providing instructions for creating a virtual environment. The recipient had to understand virtual environments, dependency management, and the relationship between the script and its requirements file.\nWith PEP 723, all of that complexity disappears. The script itself declares its requirements in its header, and tools like uv and pdm can read these declarations, provision an ephemeral environment with the specified dependencies, and execute the script automatically. This capability has largely replaced the need for pipx when running standalone scripts and has become particularly popular among DevOps engineers for writing portable automation scripts that can run anywhere without complex setup.\nHere\u0026rsquo;s a practical example of a PEP 723 script that fetches data from an API:\n#!/usr/bin/env -S uv run # /// script # requires-python = \u0026#34;\u0026gt;=3.11\u0026#34; # dependencies = [ # \u0026#34;requests\u0026lt;3\u0026#34;, # \u0026#34;rich\u0026#34;, # \u0026#34;pandas\u0026#34;, # ] # /// import requests from rich.pretty import pprint import pandas as pd def fetch_repoforge_stats(): \u0026#34;\u0026#34;\u0026#34;Fetches statistics from the RepoForge API and displays them.\u0026#34;\u0026#34;\u0026#34; try: resp = requests.get(\u0026#34;https://api.repoforge.io/stats\u0026#34;) resp.raise_for_status() data = resp.json() # Pretty print the raw data pprint(data) # Convert to DataFrame for analysis df = pd.DataFrame(data.get(\u0026#39;packages\u0026#39;, [])) print(f\u0026#34; Total packages: {len(df)}\u0026#34;) except Exception as e: print(f\u0026#34;Error fetching data: {e}\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: fetch_repoforge_stats() To execute this script, you simply run uv run dashboard.py. The tool automatically provisions an ephemeral environment with requests, rich, and pandas cached from previous runs if available, and executes the script immediately. No pip install, no virtual environment activation, no requirements.txt to manage separately. The script is entirely self-contained and portable.\nPEP 751: The Lock File Treaty One of the most contentious aspects of Python packaging has been the \u0026ldquo;Lock File War.\u0026rdquo; For years, poetry used poetry.lock, PDM used pdm.lock, and pip-tools used requirements.txt for locking. There was no interoperability between these formats. A PDM project couldn\u0026rsquo;t be easily installed by poetry, and vice versa. Each tool\u0026rsquo;s lock file format was optimized for that tool\u0026rsquo;s specific needs and implementation details, creating genuine barriers to tool migration and team collaboration when different team members preferred different tools.\nBy 2026, PEP 751 has been accepted, attempting to create a unified lock file format called pylock.toml. The goal is to have a single file format that all installers can read, allowing for reproducible installs regardless of the tool used to generate the lock file. Think of it as a \u0026ldquo;lingua franca\u0026rdquo; for dependency locking that any tool can understand, similar to how PDF serves as a universal document format regardless of which application created the original document.\nHowever, adoption in 2026 is mixed and worth understanding honestly. While the standard exists and is technically supported, major tools like uv and poetry still prefer their optimized, proprietary lock formats for day-to-day development. These formats can represent cross-platform wheels more efficiently, include tool-specific metadata that aids in faster resolution, and leverage implementation details that make installations faster. The tools generally treat PEP 751 as an export format—a way to generate a lock file for interoperability or auditing purposes—rather than a native core file format that drives daily development.\nThis creates a situation where the \u0026ldquo;standard\u0026rdquo; exists and provides value for specific use cases (auditing, security scanning, cross-tool compatibility checks) but functions more as a translation layer than as the primary lock file format most developers interact with. It\u0026rsquo;s progress toward unification, even if it\u0026rsquo;s not complete unification yet.\nFeature Comparison: Understanding Tool Trade-offs With multiple mature tools available in 2026, understanding their specific trade-offs helps you make informed decisions. The following table provides a comprehensive comparison of the major packaging tools across key features:\nFeature uv poetry PDM hatch pip Language Rust Python Python Python Python Primary Philosophy Performance \u0026amp; Unification Determinism \u0026amp; UX Standards \u0026amp; Flexibility Environments \u0026amp; Building Foundation \u0026amp; Compatibility Python Version Management Native (downloads \u0026amp; installs) Limited (via plugins/pyenv) Limited (via plugins) Limited (via plugins) No Lock File Format uv.lock (universal cross-platform) poetry.lock pdm.lock No default lock* No (use pip-tools) Build Backend Uses external (defaults to hatchling or setuptools) poetry-core pdm-backend hatchling N/A (uses setuptools or others) PEP 621 Support Native Native Native Native N/A PEP 723 Support Native No Yes No No Monorepo Support Native (Workspaces) Plugin required Limited Native (environment inheritance) No Dependency Resolution Parallel, highly optimized (PubGrub variant) Serial, rigorous (PubGrub) Serial, compliant Delegates to pip/uv Serial, reference implementation Installation Speed Fastest (10-100x) Moderate Moderate Moderate (depends on backend) Baseline Plugin Ecosystem Limited (new) Mature Growing Growing Extensive Best For Applications, monorepos, speed-critical workflows Libraries, complex dep trees, mature projects Standards compliance, hybrid workflows Library maintenance, testing matrices Universal compatibility, education *hatch generally focuses on environment matrices rather than application locking, though extensions exist.\nThis table highlights that no single tool is universally \u0026ldquo;best.\u0026rdquo; Rather, each tool excels in different scenarios. uv dominates in speed and unified workflows. poetry excels in deterministic resolution for complex dependency graphs. PDM shines in standards compliance. hatch is unmatched for environment matrices and testing. pip remains the universal foundation that everything builds upon.\nMonorepos and Workspaces: The New Architecture of Scale As Python applications in 2026 grow larger and organizations adopt more sophisticated development practices, the traditional \u0026ldquo;one repository per package\u0026rdquo; model has largely been supplanted by monorepos. Large organizations increasingly place multiple related services, libraries, and tools into a single Git repository for reasons ranging from atomic cross-project changes to simplified dependency management to better code sharing. Managing multiple interdependent Python packages in a single repository was historically painful, involving complex PYTHONPATH hacks, script-heavy CI pipelines, and constant version synchronization headaches.\nuv and poetry have both introduced specific features to address monorepo challenges, fundamentally changing how large-scale Python is architected. Understanding these capabilities helps you decide whether monorepo architecture makes sense for your organization and which tool best supports that architecture if you adopt it.\nuv Workspaces: Unified Lock Files for Consistency Borrowing concepts from Rust\u0026rsquo;s Cargo workspaces, uv introduced native workspace support that has become the gold standard for Python monorepos in 2026. The key innovation is the unified lock file. Instead of each package in your monorepo maintaining its own lock file that might drift out of sync with others, uv creates a single lock file at the repository root that covers all packages simultaneously.\nHere\u0026rsquo;s what a typical uv workspace structure looks like:\n/my-monorepo ├── pyproject.toml # Workspace root configuration ├── uv.lock # Single unified lockfile for entire repo ├── packages/ │ ├── core-lib/ │ │ └── pyproject.toml # Package-specific configuration │ ├── api-service/ │ │ └── pyproject.toml # Depends on core-lib │ └── worker-service/ │ └── pyproject.toml # Depends on core-lib The root pyproject.toml defines the workspace members:\n# /my-monorepo/pyproject.toml [tool.uv.workspace] members = [\u0026#34;packages/*\u0026#34;] [project] name = \u0026#34;my-monorepo\u0026#34; requires-python = \u0026#34;\u0026gt;=3.12\u0026#34; Why this matters: When a developer runs uv sync at the repository root, uv resolves dependencies for all packages simultaneously. It ensures that if core-lib uses pydantic==2.8.0, both api-service and worker-service also use that exact version. This guarantees consistency across your entire fleet of services and eliminates \u0026ldquo;diamond dependency\u0026rdquo; issues between internal components where different services depend on conflicting versions of shared dependencies.\nThe unified lock file approach means that when you update a dependency in one package, uv re-resolves the entire dependency graph to ensure nothing breaks anywhere in the monorepo. This prevents the subtle bugs that occur when different services test against different dependency versions locally but get deployed with different versions in production. It allows for \u0026ldquo;editable\u0026rdquo; installs of internal packages by default, so changes in core-lib are immediately reflected in api-service without re-installation or build steps.\nCI/CD Strategies for Monorepos Monorepos introduce complexity in CI/CD pipelines. If one package changes, you typically don\u0026rsquo;t want to rebuild and redeploy everything—that would negate many of the efficiency benefits of the monorepo structure. You need selective building and testing based on what actually changed.\nuv supports intelligent caching and selective operations that make this practical:\n# Example CI workflow for a monorepo # 1. Sync the environment (uses cached wheels if lockfile hasn\u0026#39;t changed) # The --frozen flag ensures no resolution happens, only installation uv sync --frozen # 2. Run tests only for the \u0026#39;api-service\u0026#39; package # This runs only the tests for the specific package that changed uv run --package api-service pytest # 3. Build only changed packages # You can use git diff to determine which packages need rebuilding uv build --package api-service Tools like RepoForge.io play a crucial role in monorepo CI performance by serving as the central cache for built wheels of internal packages. If core-lib hasn\u0026rsquo;t changed between builds, the CI pipeline pulls the pre-built wheel from your private repository cache rather than rebuilding it from source. This can save significant compute time, especially for packages with C extensions or complex build processes. The combination of uv\u0026rsquo;s workspace support, intelligent caching, and a fast private repository creates CI pipelines that remain fast even as monorepos grow to dozens of internal packages.\nComparison with poetry Monorepo Support poetry supports similar monorepo functionality but relies on \u0026ldquo;path dependencies\u0026rdquo; and external plugins like poetry-plugin-monorepo or poetry-monoranger-plugin to achieve true workspace behavior. The approach works but often requires separate lock files for each sub-project unless you use specific plugins, which can lead to version drift between services in the same repository.\n# poetry approach to monorepo dependencies [tool.poetry.dependencies] python = \u0026#34;^3.11\u0026#34; my-core-lib = { path = \u0026#34;../core-lib\u0026#34;, develop = true } While functional, the poetry approach in 2026 is generally viewed as less cohesive than uv\u0026rsquo;s native implementation for monorepo scenarios. The plugin ecosystem is mature and can handle complex cases, but it requires more configuration and manual synchronization. For organizations specifically optimizing for monorepo architecture, uv\u0026rsquo;s unified lock file approach is generally considered superior for maintaining rigorous consistency.\nChoosing Your Tools: Practical Recommendations With so many options available, choosing the right combination of tools can feel overwhelming. However, the decision becomes clearer when you consider your specific circumstances, your team\u0026rsquo;s experience level, your project\u0026rsquo;s requirements, and your organization\u0026rsquo;s constraints. Rather than looking for a single perfect tool that does everything, think about which combination of specialized tools will give you the best outcomes for your particular situation.\nFor new Python projects, especially applications like web services, data pipelines, or internal tools, starting with poetry or hatch provides immediate value. These tools offer integrated workflows that handle dependency management, environment management, and package building through a single interface. poetry has the larger community and more mature ecosystem at this point, with extensive documentation and widespread adoption that means you\u0026rsquo;ll find answers to common questions easily. hatch offers more flexibility and closer adherence to standards, which can matter if you need to integrate with existing tooling or want to avoid potential lock-in to tool-specific approaches. Either choice will serve you well for most application development scenarios, and the decision often comes down to which tool\u0026rsquo;s philosophy resonates more with your team.\nFor existing projects where you already have established workflows and infrastructure, the migration path matters enormously. Completely rewriting your build and deployment processes to adopt a new integrated tool creates risk and consumes time that might be better spent on features or other improvements. If you\u0026rsquo;re currently using requirements.txt files and pip, adopting pip-tools to add lock file functionality represents the lowest-friction upgrade path. You keep your existing workflows intact, your deployment scripts continue working unchanged, and your team doesn\u0026rsquo;t need to learn an entirely new tool ecosystem. You simply add one compilation step that generates pinned requirements from your input requirements, gaining reproducibility without disruption. Once you\u0026rsquo;re comfortable with lock files and see the value they provide, you can evaluate whether moving to a more integrated tool makes sense for your situation, but you\u0026rsquo;re not forced to make that leap all at once.\nFor large organizations with complex requirements that span multiple teams, multiple projects, and multiple deployment environments, using different tools for different purposes often produces the best results. You might use pip-compile for lock file generation because it integrates seamlessly with your existing deployment pipeline and provides the flexibility you need for managing different environment types. You might use uv for fast package installation in CI where speed directly translates to infrastructure cost savings. You might use twine for publishing to your private repository because it provides secure, reliable uploads that work consistently across different package types. This mix-and-match approach lets you optimize each part of your workflow independently, and it prevents you from being blocked by a single tool\u0026rsquo;s limitations in one area affecting your entire stack.\nFor library development, where you\u0026rsquo;re creating packages that other developers will depend on, the considerations shift somewhat. You need excellent support for building distributable packages, managing version numbers across releases, and testing against multiple Python versions and dependency combinations. hatch excels in this role with its environment management capabilities that make testing against different configurations straightforward. poetry also works well for library development, though its opinions about project structure can sometimes feel constraining. Many library maintainers still use setuptools directly with pip-tools for dependency management, finding this combination flexible enough to handle their specific needs while remaining simple and transparent. The choice often depends on how much you value integrated features versus maximum flexibility and control.\nIf your team values speed above almost everything else and you have relatively straightforward requirements without unusual edge cases, replacing pip with uv everywhere can yield immediate benefits. The installation speed improvements are real and significant, particularly for projects with many dependencies or teams that create and destroy environments frequently. However, remember the CI caveat we discussed earlier. If your CI runs on minimal virtual machines with limited CPU allocation, uv\u0026rsquo;s advantage diminishes because its parallelism can\u0026rsquo;t fully exploit single-core environments. In these cases, the combination of uv with a fast private repository like RepoForge addresses both bottlenecks, giving you back the dramatic improvements even in constrained environments.\nFor organizations handling sensitive code or operating in regulated industries with compliance requirements, private package repositories aren\u0026rsquo;t optional. They\u0026rsquo;re essential infrastructure that provides security, control, and auditability. Every package that reaches your developers passes through your repository first, where you can scan it, approve it, and ensure it meets your standards. When deciding on tooling, ensure your chosen tools integrate cleanly with private repositories. All the major tools we\u0026rsquo;ve discussed support custom package indexes, but the ease of that integration varies. pip-tools and uv make it trivial because they\u0026rsquo;re built on pip\u0026rsquo;s foundation. poetry requires configuration but works well once set up. The key is ensuring your tooling supports your security requirements without creating friction that developers will want to work around.\nBest Practices for Python Packaging in 2026 Regardless of which specific tools you choose for your Python packaging workflow, certain practices remain universally valuable and provide benefits that compound over time. These practices represent lessons learned from years of production Python development across organizations of all sizes, and adopting them early in your project\u0026rsquo;s lifecycle pays dividends throughout its lifetime.\nAlways use lock files or equivalent mechanisms to ensure reproducible environments. The difference between \u0026ldquo;it works on my machine\u0026rdquo; and \u0026ldquo;it works everywhere\u0026rdquo; often comes down to dependency version mismatches that lock files prevent. When you commit a lock file to version control, you\u0026rsquo;re guaranteeing that everyone on your team, your CI system, and your production deployment all use identical dependency versions. This eliminates an entire class of bugs where code fails in production because production happened to resolve a dependency differently than development did. Whether you use poetry\u0026rsquo;s poetry.lock, pip-compile\u0026rsquo;s requirements.txt, or another tool\u0026rsquo;s lock file format, the key is committing those lock files and ensuring they\u0026rsquo;re actually used during installation.\nPin your dependencies tightly in production but stay relatively looser in development, though this might seem contradictory at first glance. In production, you want absolute stability and reproducibility. You want the exact same code running every time you deploy, which means the exact same dependency versions. Your lock files provide this guarantee. In development, however, you want to catch issues with new dependency versions relatively early, before they become production problems. Your lock files should still pin versions to ensure consistency across your team, but you should regularly update those lock files to pull in newer versions and test them in your development and staging environments. This practice helps you discover breaking changes or new bugs in dependencies before they impact production, while still maintaining reproducibility when you need it.\nSeparate your direct dependencies from your transitive dependencies in your thinking and in your tooling configuration. Your direct dependencies are the packages you explicitly chose to use because they provide functionality your code needs. Your transitive dependencies are what those packages depend on, which you use indirectly through your direct dependencies. Tools that maintain this separation, like pip-compile with its .in and .txt files or poetry with its distinction between dependencies and lock file contents, make it easier to understand and manage your dependency graph. When a security vulnerability is discovered in a transitive dependency, this separation helps you trace which direct dependency pulled it in and whether you can update or replace that direct dependency to get a safe version of the transitive one.\nUse a private Python package registry for any code that\u0026rsquo;s internal to your organization, even if you\u0026rsquo;re a small team. The temptation to use git dependencies, vendor code directly into your repositories, or publish internal code to public PyPI with obscure names creates problems that multiply as your projects grow. A private PyPI server like RepoForge.io makes internal package distribution so straightforward that there\u0026rsquo;s little reason not to establish this pattern from the beginning. Your internal packages are installed the same way as public ones, using the same tools and workflows, which reduces cognitive load and makes onboarding new team members simpler. The security benefits we discussed earlier matter just as much for small teams as for large organizations.\nInvest in fast CI/CD pipelines from the start rather than treating slow builds as an inevitable annoyance. The speed of your packaging tools directly impacts your deployment frequency, your ability to quickly roll back bad changes, and your developers\u0026rsquo; productivity when they\u0026rsquo;re waiting for tests to complete before merging pull requests. Tools like uv can cut CI times significantly, but remember to address both bottlenecks: CPU utilization through parallel operations and network latency through fast package indexes. Using uv with a low-latency private repository gives you multiplicative improvements rather than just additive ones. When you\u0026rsquo;re running hundreds or thousands of builds per day, these improvements translate directly to infrastructure cost savings and faster iteration cycles.\nDocument your tooling choices and the reasoning behind them for your team and future maintainers. Python packaging can be confusing, especially for developers new to Python or new to your organization. When someone joins your team and wonders why you use pip-compile instead of poetry, or why you have a private repository, or why your CI uses uv instead of pip, having documentation that explains the trade-offs you considered and the benefits you\u0026rsquo;re getting makes their onboarding smoother and reduces the likelihood that they\u0026rsquo;ll propose changes that undo valuable patterns you\u0026rsquo;ve established. Your documentation should include setup instructions that actually work, common workflows that developers need daily, and troubleshooting tips for your specific tool combination. Keep this documentation in your repositories where it\u0026rsquo;s versioned alongside your code and can evolve as your practices evolve.\nRegularly audit your dependencies for security vulnerabilities and license compliance issues. Tools like Safety for security scanning and pip-licenses for license checking should run automatically in your CI pipeline, failing builds if they detect problems. Don\u0026rsquo;t wait for a security incident to start thinking about dependency hygiene. When you use a private repository, you can implement these checks at the repository level, scanning packages as they\u0026rsquo;re published and blocking problematic packages before they reach any developers or make it into any builds. This proactive approach is far superior to reactive security patching after vulnerabilities are discovered in production.\nLooking Forward: The Future of Python Packaging The Python packaging ecosystem in 2026 is healthier and more capable than it has ever been, but it\u0026rsquo;s certainly not finished evolving. Understanding where the ecosystem is heading helps you make tooling decisions that will age well and positions you to take advantage of new capabilities as they emerge. Several clear trends are shaping the next evolution of Python packaging, and being aware of these trends informs the choices you make today.\nThe Rust Revolution Continues The trend toward Rust-based tooling shows no signs of slowing down and may actually be accelerating. We\u0026rsquo;ve seen this with uv bringing dramatic speed improvements to package installation, and with ruff revolutionizing Python linting and formatting. The performance characteristics of compiled Rust code, combined with Rust\u0026rsquo;s memory safety guarantees, make it an attractive choice for building the next generation of Python tooling. Expect to see more Python tools rewritten in Rust for performance over the coming years, potentially including build systems, test runners, and other parts of the development workflow. This doesn\u0026rsquo;t mean Python-based tools are going away or becoming obsolete. Many excellent tools remain Python-based and work perfectly well for their intended purposes. However, the performance bar is rising, and developers are beginning to expect that their tools should be fast enough to stay out of the way rather than being bottlenecks that interrupt flow.\nStandards Convergence and Interoperability Standards convergence is happening across the ecosystem, which is valuable even as the number of tools continues to grow. While the profusion of tools might seem chaotic from the outside, they\u0026rsquo;re increasingly standardizing on common formats and approaches that make the ecosystem more interoperable. The pyproject.toml file has emerged as the standard place for Python project configuration, regardless of which specific tools you use. Build backends are standardizing on the PEP 517 interface, which means your choice of build backend is increasingly decoupled from your choice of other tools. These standards make the ecosystem more interoperable even as tools specialize in different areas. You can switch from one dependency management tool to another with less friction than was possible a few years ago, and projects can be maintained by developers using different tool preferences without conflict.\nThe Death of setup.py By 2026, setup.py is effectively dead for new projects. While legacy projects still contain it and will continue to work, uv and modern linters now actively warn or error when initiating new projects with setup.py-based configuration. The ecosystem has fully transitioned to declarative configuration via pyproject.toml, and this transition represents more than just a format change. It represents a fundamental shift in how we think about package building and security.\nThe \u0026ldquo;dynamic\u0026rdquo; capabilities of setup.py—the ability to run arbitrary Python code during package installation—were a major security vector. A malicious package could execute code on your machine simply during the dependency resolution phase, before you had any chance to review what it actually did. This capability enabled attacks where typosquatted packages would steal environment variables, AWS credentials, or SSH keys during installation, long before the victim realized anything was wrong.\nThe move to declarative pyproject.toml configuration eliminates this attack vector. Packages declare their metadata and dependencies in a static format that can be inspected without execution. Build systems still have the ability to run code during the actual build process, but that\u0026rsquo;s a more controlled phase that happens explicitly when you choose to build a package, not implicitly during dependency resolution. This improves security by preventing malicious code execution during what should be a safe operation—checking if dependencies can be satisfied.\nPEP 703: The No-GIL Future One of the most significant technical developments reaching maturity in 2026 is PEP 703, which makes the Global Interpreter Lock optional in Python. The \u0026ldquo;free-threaded\u0026rdquo; or \u0026ldquo;No-GIL\u0026rdquo; builds of Python are becoming mainstream, fundamentally changing Python\u0026rsquo;s concurrency story. For the first time, Python code can truly utilize multiple CPU cores for CPU-bound work without resorting to multiprocessing or external tools written in other languages.\nThis has massive implications for packaging. Extension modules written in C, Rust, or C++ need to distribute wheels specifically compiled for free-threaded Python. The packaging ecosystem has had to evolve to support these new ABI tags (for example, cp313t for threaded Python 3.13). Tools like cibuildwheel have been updated to build wheels for both standard and free-threaded Python, and private repositories like RepoForge.io have updated their infrastructure to support hosting and serving these additional wheel variants.\nPackage maintainers are currently in a transition period, ensuring their wheels are compatible with this concurrent future. For some packages, especially pure Python packages that don\u0026rsquo;t use C extensions, the transition is transparent. For packages with C extensions, maintainers need to ensure their code is thread-safe without the GIL\u0026rsquo;s protection, which can require significant refactoring. The packaging tools are evolving to make this transition as smooth as possible, automatically building appropriate wheel variants and allowing developers to specify which Python variants their packages support.\nPEP 2026: Calendar Versioning for Python A significant shift under active consideration (and proposed in PEP 2026) is the move to Calendar Versioning (CalVer) for Python itself. The proposal suggests that the version following Python 3.14 would jump to Python 3.26, making the 2026 release explicitly Python 3.26. This change targets the confusion around support lifecycles that has plagued Python version planning for years.\nThe genius of CalVer is that knowing Python 3.26 was released in 2026 makes it immediately obvious when it will reach End-of-Life—typically five years later, in 2031. This simplifies communication with management and clients about upgrade cycles and makes capacity planning more straightforward. While controversial among purists who prefer Semantic Versioning where version numbers indicate breaking changes, the pragmatism of CalVer aligns with the broader industry trend seen in Ubuntu, Unity, and other major projects.\nFrom a packaging perspective, CalVer would simplify the requires-python metadata in pyproject.toml files and make it clearer which Python versions are current versus legacy. It would help organizations plan their infrastructure upgrades by making support windows explicit in the version number itself.\nThe Maturing Enterprise Ecosystem Private package repositories are becoming more common and more sophisticated as organizations adopt Python more widely for business-critical applications. The need for controlled, secure, internal package distribution grows as Python moves from being a scripting language used by a few specialists to being a primary development language across entire engineering organizations. Expect to see more features around security scanning, access control, integration with identity providers, and connections to other development tools like vulnerability databases and compliance frameworks. Services like RepoForge.io are evolving to meet these emerging needs, making enterprise Python packaging as smooth and reliable as the public PyPI experience while providing the security and control that businesses require. The integration between private repositories and development tools will deepen, with better support for monorepo workflows, sophisticated caching strategies, and seamless integration with container registries and artifact storage systems.\nThe security posture of the entire ecosystem is improving through both technical measures and community awareness. PyPI is implementing stronger verification for package maintainers, including support for trusted publishing workflows that reduce the risk of credential theft. Package signing and verification mechanisms are becoming more sophisticated, giving developers better tools to verify that packages haven\u0026rsquo;t been tampered with between publication and installation. Security scanning tools are becoming more accurate and better integrated into development workflows, catching vulnerabilities earlier in the development process rather than in production. These improvements don\u0026rsquo;t eliminate security risks entirely, but they raise the difficulty bar for attackers and reduce the window of opportunity for successful attacks.\nConclusion: Embrace the Ecosystem and Build Secure, Fast Workflows The state of Python packaging in 2026 is characterized by choice, performance, maturity, and an increasing awareness of security concerns that must be addressed as Python moves from hobby projects to business-critical infrastructure. While the number of tools available can seem overwhelming when you\u0026rsquo;re first trying to understand the landscape, each tool emerged to solve real problems that developers face in production environments. Understanding these tools, their trade-offs, and how they fit together empowers you to build better, faster, and more secure Python applications that can scale from prototypes to production systems serving millions of users.\nThe key insight that should guide your approach to Python packaging in 2026 is that you don\u0026rsquo;t need to use just one tool for everything. Modern Python development often involves a thoughtful combination of specialized tools, each chosen for its particular strengths. You might use pip-compile for generating lock files because it integrates seamlessly with your existing deployment pipeline and provides the transparency you value. You might use uv for fast installation in CI environments because the speed improvements translate directly to infrastructure cost savings and faster deployment cycles. You might use setuptools for building packages because its maturity and extensive documentation give you confidence when dealing with complex build requirements. You might use twine for distribution because its security focus and simplicity make it reliable for both manual and automated releases. And critically, you should be using a private repository like RepoForge.io for internal packages and as a security layer for external dependencies, protecting your organization from typosquatting attacks and giving you control over what code reaches your production systems.\nThe security discussion we covered earlier deserves emphasis because it\u0026rsquo;s often overlooked until an incident forces organizations to take it seriously. Publishing everything to PyPI or installing directly from PyPI without any vetting creates risks that grow more severe as your systems become more critical. Typosquatting attacks are real, they\u0026rsquo;re increasing in sophistication, and they can compromise your infrastructure in ways that are difficult to detect until significant damage has occurred. Using a private repository isn\u0026rsquo;t just about hosting internal code, though that\u0026rsquo;s important too. It\u0026rsquo;s about creating a security perimeter around your dependency supply chain, ensuring that packages are vetted before they reach your developers and your production systems. This security layer doesn\u0026rsquo;t need to create friction in your development workflow when implemented thoughtfully. With services like RepoForge.io, the workflow remains identical to what developers expect, but with security guarantees that direct PyPI usage can\u0026rsquo;t provide.\nPerformance matters more than many developers initially realize, and the improvements we\u0026rsquo;ve discussed compound in ways that significantly impact your organization\u0026rsquo;s efficiency and costs. When your CI pipeline takes two minutes instead of five minutes because you optimized package installation, that three-minute savings multiplies across every pull request, every deployment, and every developer making changes. Over a year with hundreds of developers and thousands of builds, those minutes add up to substantial cost savings in CI infrastructure and, more importantly, faster iteration cycles that let you ship features more quickly. The combination of fast installation tools like uv with low-latency private repositories like RepoForge addresses both major bottlenecks in dependency installation, giving you performance improvements that remain substantial even in CPU-constrained CI environments.\nStart with the basics that provide the most value for the least disruption to your existing workflows. Implement lock files if you haven\u0026rsquo;t already, whether through pip-compile, poetry, or another tool that makes sense for your situation. Set up a private repository for internal code and, if appropriate for your security requirements, for curating external dependencies. Optimize your CI pipelines for speed because every second you save compounds across your entire development organization. These foundational improvements provide immediate value and create a solid platform for adopting additional tools and practices as your needs evolve.\nAs you build out your Python packaging infrastructure, remember that the ecosystem continues to evolve and improve. New tools will emerge, existing tools will add capabilities, and standards will continue to converge around common approaches that make the ecosystem more interoperable. The decisions you make today should account for this evolution by favoring standards-based approaches when possible, documenting your choices so future maintainers understand your reasoning, and remaining flexible enough to adopt better tools when they become available. The Python packaging ecosystem has never been better equipped to support projects of any scale, from simple scripts to massive applications with hundreds of dependencies and complex deployment requirements. Choose the tools that fit your workflow and your constraints, stay current with ecosystem developments, and enjoy the benefits of a mature, innovative, and increasingly secure packaging ecosystem that continues to improve year after year.\n","permalink":"https://repoforge.io/blog/posts/the-state-of-python-packaging-in-2026/","summary":"\u003cp\u003e\n\n\u003cimg src=\"/blog/images/posts/the-state-of-python-packaging-in-2026/IB1-standards-blog-2023-09-18.jpg\" alt=\"Image: https://xkcd.com/927/\" /\u003e\n\u003c/p\u003e\n\u003cp\u003eIf you\u0026rsquo;ve ever felt overwhelmed by Python\u0026rsquo;s packaging ecosystem, you\u0026rsquo;re experiencing what the legendary XKCD comic #927 perfectly captured. The comic shows a familiar scenario: fourteen competing standards exist, someone declares \u0026ldquo;this is ridiculous, we need one universal standard,\u0026rdquo; and the final panel reveals there are now fifteen competing standards. For nearly two decades, Python packaging seemed trapped in exactly this recursive loop.\u003c/p\u003e\n\u003cp\u003eWe transitioned from distutils to \u003ccode\u003esetuptools\u003c/code\u003e, introduced \u003ccode\u003epip\u003c/code\u003e for installation and \u003ccode\u003evirtualenv\u003c/code\u003e for isolation, experimented with \u003ccode\u003epipenv\u003c/code\u003e for lock files, embraced \u003ccode\u003epoetry\u003c/code\u003e for deterministic resolution, and explored \u003ccode\u003epdm\u003c/code\u003e for standards compliance. Each tool promised to unify workflows around package installation, dependency resolution, and distribution. Each contributed valuable concepts like deterministic builds and standardized metadata, yet often created distinct, incompatible silos.\u003c/p\u003e","title":"The State of Python Packaging in 2026: A Comprehensive Guide"},{"content":"Introduction Conda is a popular package and environment management system primarily used with Python. It\u0026rsquo;s widely used in data science, scientific computing, AI, and bioinformatics, because it facilitates portability and reproducibility of (data) science workflows.\nHowever, if your team needs to host private Conda packages, the typical recommendation is to build and manage your own Conda channel. While self-hosting a Conda channel is possible, it requires ongoing maintenance and infrastructure costs. Surprisingly, major package managers like Artifactory and PackageCloud don\u0026rsquo;t natively support Conda.\nThis guide will show you how to create a private Conda channel step by step, including:\nHow to build a simple Conda package (no prior experience needed). How to set up a private Conda repository using RepoForge. How to upload and install Conda packages from your private channel. Step 1: Build a Simple Conda Package Before setting up a private Conda channel, we need a sample package. We\u0026rsquo;ll create a simple \u0026ldquo;hello-world\u0026rdquo; Conda package that:\nContains a Python script (hello.py) that prints \u0026ldquo;Hello, Conda!\u0026rdquo;. Includes a meta.yaml file defining package metadata. Is built using Conda Build. 1.1 Install Conda Build Tools Make sure you have Conda installed. If not, install Miniconda or Anaconda.\nThen, install the Conda build tools:\nconda install -y conda-build anaconda-client 1.2 Create the Package Structure Run the following to create a directory for our package:\nmkdir -p ~/conda-packages/hello-world cd ~/conda-packages/hello-world Now, create the Python script:\n# hello-world/hello.py def main(): print(\u0026#34;Hello, Conda!\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: main() Next, define the package metadata. It should be called meta.yaml and stored in the same folder as the above Python script.\npackage: name: hello-world version: \u0026#34;1.0.0\u0026#34; source: path: . build: script: python -m hello requirements: run: - python about: summary: \u0026#34;A simple hello-world package for Conda\u0026#34; license: MIT 1.3 Build the Conda Package Now, build the package using conda build:\nconda build . --output-folder dist --output The package should now be built in the dist folder. The above command should output the specific path to the build conda package to be uploaded — it should look something like this:\ndist/osx-arm64/hello-world-1.0.0-0.conda This package can now be uploaded to RepoForge.\nStep 2: Create a Private Conda Repository on RepoForge Now that we have a package, we need a private Conda repository to host it.\n2.1 Sign Up for RepoForge If you haven\u0026rsquo;t already, sign up for a free trial and create a private conda channel on RepoForge.io.\n2.2 Create a New Conda Channel Go to the RepoForge dashboard.\nClick \u0026ldquo;Create new Conda channel\u0026rdquo; and give it a name (I used hello-world)\nStep 3: Upload the Conda Package to RepoForge Once you\u0026rsquo;ve created your private Conda channel, you can click on the Show push commands button to see how to publish packages to your Conda channel, which will show you a code snippet that looks something like this:\nimport requests with open(\u0026#34;dist/osx-arm64/hello-world-1.0.0-0.conda\u0026#34;, \u0026#34;rb\u0026#34;) as f: files = {\u0026#39;file\u0026#39;: f} auth = (\u0026#34;your-email@example.com\u0026#34;, \u0026#34;$REPOFORGE_PASSWORD\u0026#34;) response = requests.post( \u0026#39;https://api.repoforge.io/conda/unique-hash-id/my-channel\u0026#39;, files=files, auth=auth ) assert response.status_code == 200 Essentially, all you need to do is POST your Conda package to the unique RepoForge.io URL shown in the above code snippet. It should just be a case of executing the above code, ensuring that you replace the following parts:\nunique-hash-id with your organisation\u0026rsquo;s unique ID. my-channel with the name of your desired Conda channel, e.g. hello-world your-email@example.com and $REPOFORGE_PASSWORD with your RepoForge.io credentials. Finally, ensure the file path is correct — it should match the output of your conda build command earlier Once you\u0026rsquo;ve run the upload script, refresh the view in the RepoForge.io dashboard — you should see your package has been uploaded!\nStep 4: Configure Conda to Use Your Private Channel Now, let\u0026rsquo;s install the package from your private Conda repository.\nIn the RepoForge dashboard, click on the info icon next to your package to see the installation commands:\n4.1 Install the conda-auth plugin This step is only required if you created a private conda channel — if you\u0026rsquo;re using the free version of RepoForge, you won\u0026rsquo;t need to do this (although anybody with the link will be able to access your package)\nThe command for installing it is the first one in the above dialog:\nconda install --name base --channel conda-forge conda-auth 4.2 Add Your Private RepoForge Channel To configure Conda to use your private RepoForge repository, run the next snippet from the above dialog:\nconda config --set custom_channels.hello-world \u0026lt;your-repoforge-repo-url\u0026gt; 4.3 Login with Conda auth Again, this is only required for non-public Conda channels.\nconda auth login -b hello-world --username \u0026lt;repoforge username\u0026gt; --password $REPOFORGE_PASSWORD 4.4 Install the Package Now, install hello-world from your private Conda channel:\nconda install -c hello-world hello-world=1.0.0 4.5 Verify Installation You can now use the below command to verify that it worked\nconda list -n conda-test | grep hello hello-world 1.0.0 0 hello-world Done! Your private Conda package is now hosted, installed, and working correctly.\nNext Steps: Automate Package Publishing If you\u0026rsquo;re regularly updating and distributing Conda packages, you can:\nAutomate uploads with a simple script. Set up CI/CD integration to publish new package versions automatically. Control access with fine-grained permissions in RepoForge. To explore more advanced workflows, check out the RepoForge Conda documentation.\n","permalink":"https://repoforge.io/blog/posts/creating-a-private-conda-channel-with-repoforge/","summary":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eConda is a popular package and environment management system primarily used with Python. It\u0026rsquo;s widely used in data science, scientific computing, AI, and bioinformatics, because it facilitates portability and reproducibility of (data) science workflows.\u003c/p\u003e\n\u003cp\u003eHowever, if your team needs to host private Conda packages, the typical recommendation is to build and manage your own Conda channel. While self-hosting a Conda channel is possible, it requires ongoing maintenance and infrastructure costs. Surprisingly, major package managers like Artifactory and PackageCloud don\u0026rsquo;t natively support Conda.\u003c/p\u003e","title":"How to Create a Private Conda Channel with RepoForge.io"},{"content":"This tutorial covers:\nCreating a simple Debian package using fpm Uploading it to a private APT repository on RepoForge.io Installing it from RepoForge in a Docker container Step 1 – Create a Debian package For this tutorial, we are going to make a simple hello world application. First, let\u0026rsquo;s create a working directory called hello, and add a subfolder called usr inside it, then another subfolder called bin inside that. Then, we\u0026rsquo;re going to add a file called hello inside of the bin folder. Your folder structure should look like this:\n/hello /usr /bin hello Note that we have deliberately NOT added an extension to the hello file. Next, let\u0026rsquo;s open our hello file and add some extremely simple Python code to it:\n#!/usr/bin/env python3 print(\u0026#39;hello world!\u0026#39;) Next, let\u0026rsquo;s make the hello file executable, by running this command:\nsudo chmod +x hello And that\u0026rsquo;s it! Our script is now ready to be packaged. There are a number of different ways to convert this script into a Debian package, but probably the easiest way is to use fpm, which is a tool that you can use to generate all sorts of package formats very easily.\nYou can install fpm using gem, like this:\ngem install fpm Once installed, you can run the below command from your working directory root to generate a debian package:\nfpm -s dir -t deb -n hello --architecture all usr/bin/hello This should produce a file called hello_1.0_all.deb - this is the Debian package that we will be deploying. Your folder structure should now look like this:\n/hello /usr /bin hello hello_1.0_all.deb Before we upload it anywhere, let\u0026rsquo;s do a quick test to make sure it works as expected. To do this, I\u0026rsquo;m going to spin up a simple Dockerfile, add the package file to it, and try to install and run it from the file.\nFirst, let\u0026rsquo;s create the Dockerfile in the same folder as your .deb package:\n# we will use the official Ubuntu base image for this FROM ubuntu:20.04 # we will update our sources and install Python, so our script can be run RUN apt-get update -y \u0026amp;\u0026amp; apt-get install python3 -y # add our debian file to the docker container COPY hello_1.0_all.deb ./ # install the script from the file RUN dpkg -i hello_1.0_all.deb # tell the docker container to run our command on startup CMD hello Let\u0026rsquo;s build the docker container and run it to make sure everything is working as expected:\n# docker build -t hello . ... =\u0026gt; =\u0026gt; naming to docker.io/library/hello 0.0s # docker run hello hello world! Everything is looking good — let\u0026rsquo;s move on to the next stage.\nStep 2 – Deploy your package to RepoForge If you haven\u0026rsquo;t already, you\u0026rsquo;ll first need to sign up for a free trial to get a private APT repository on RepoForge.io. Once you\u0026rsquo;ve done so, log into your account and click on Debian in the left side navigation bar:\nOnce you\u0026rsquo;ve done that, click on the Show me how to publish packages button in the top right hand corner.\nThis dialog will give you your unique RepoForge Debian repository URL — we\u0026rsquo;ll use this link to both publish and download our Debian package — make a note of it somewhere as you\u0026rsquo;ll need this later.\nThe first step is simply to send a POST request to the above URL with the .deb file that you have generated. There are a number of ways to do this, but here\u0026rsquo;s a simple example with Python:\nimport requests with open(\u0026#34;hello_1.0_all.deb\u0026#34;, \u0026#34;rb\u0026#34;) as f: response = requests.post( # change the below line to add your own RepoForge.io URL \u0026#34;https://api.repoforge.io/debian/your-hash-id/\u0026#34;, files={\u0026#34;content\u0026#34;: f}, # use __token__ as username and your access token as password auth=(\u0026#34;__token__\u0026#34;, \u0026#34;$REPOFORGE_ACCESS_TOKEN\u0026#34;) ) assert response.status_code == 200 You\u0026rsquo;ll need to create an access token in the RepoForge dashboard under Access Tokens, with Debian write permissions.\nOnce you\u0026rsquo;ve run that, refresh the RepoForge.io dashboard in your browser, and you should find that your package now exists!\nAnd that\u0026rsquo;s it for this stage — our Debian package is now in the cloud, ready to be installed.\nStep 3 – Install your package on a target machine Once again, I\u0026rsquo;m going to use a Dockerfile with the Ubuntu base image as a target to install my package. Other linux distros may have slightly different steps, but the general process is the same. You will need to:\nDownload and sign the RepoForge.io public key Add RepoForge as an apt source Add your RepoForge credentials to the target machine The RepoForge.io public key All Debian apt sources must be signed with a GPG key in order to be considered trusted by Ubuntu. We\u0026rsquo;ll therefore first need to download the RepoForge.io public key from the below path. You\u0026rsquo;ll need to change the URL to use your own unique RepoForge URL:\nhttps://api.repoforge.io/debian/my-repoforge-hash/repoforge.public.key This file will need to be stored in apt\u0026rsquo;s trusted GPG key folder, which on Ubuntu is /etc/apt/trusted.gpg.d/. In order for Ubuntu to be able to sign requests properly, you\u0026rsquo;ll also need to install the gpg, gnupg and ca-certificates package.\nLet\u0026rsquo;s start our Dockerfile by installing these dependencies, as well as the wget package, and downloading the public key to our container.\nFROM ubuntu:20.04 # install the dependencies we need RUN apt-get update \u0026amp;\u0026amp; apt-get install wget gpg gnupg ca-certificates -y # download the public key to the right place (UPDATE YOUR REPOFORGE URL) RUN wget https://api.repoforge.io/debian/my-repoforge-hash/repoforge.public.key -O \\ /etc/apt/trusted.gpg.d/repoforge.public.key Adding RepoForge as an apt source On Ubuntu and all other Debian based distributions, the apt software repositories are defined in the /etc/apt/sources.list file or in separate files under the /etc/apt/sources.list.d/ directory. Therefore, the next step of our Dockerfile is to add a new source file for RepoForge in that directory:\nRUN echo \\ \u0026#34;deb [signed-by=/etc/apt/trusted.gpg.d/repoforge.public.key] \\ https://api.repoforge.io/debian/my-repoforge-hash/ hello main\u0026#34; \\ \u0026gt; /etc/apt/sources.list.d/repoforge.list In the above statement, the signed-by key tells Ubuntu where to find the RepoForge public key we downloaded earlier.\nAdding your RepoForge credentials By default, any packages that you upload to RepoForge are private. This means that you need to provide your RepoForge credentials to apt so that it has the right permissions to pull the package.\nIt is possible to make them public by flicking the Private switch in the RepoForge dashboard. If you do that, then you won\u0026rsquo;t need to complete this step at all.\nOnce again, apt has a special folder for storing things like this. On Ubuntu the folder is at /etc/apt/auth.conf.d/.\nIn the interest of security, you should never add your access token directly to your Dockerfile. For the sake of this tutorial we are using a Docker build arg instead. However even this isn\u0026rsquo;t ideal – you should probably use Docker secrets.\nWith all that in mind, lets add the next bit to our Dockerfile:\nARG REPOFORGE_ACCESS_TOKEN RUN echo \u0026#34;machine https://api.repoforge.io\\n \\ login __token__\\n \\ password ${REPOFORGE_ACCESS_TOKEN}\u0026#34; \u0026gt; /etc/apt/auth.conf.d/repoforge Finally, we should be able to wrap it up by adding the apt-get update and apt-get install commands to our Dockerfile:\nRUN apt-get update \u0026amp;\u0026amp; apt-get install hello -y Putting it all together Our completed Dockerfile should now look something like this:\nFROM ubuntu:20.04 # don\u0026#39;t do the below in production - use build secrets instead! ARG REPOFORGE_ACCESS_TOKEN # update system and install dependencies required for signing the public key RUN apt-get update RUN apt-get install wget gpg gnupg ca-certificates -y # Download the RepoForge.io public key and save it in /etc/apt/trusted.gpg.d/ RUN wget https://api.repoforge.io/debian/your-hash-id/repoforge.public.key -O \\ /etc/apt/trusted.gpg.d/repoforge.public.key # Add RepoForge as an apt source RUN echo \\ \u0026#34;deb [signed-by=/etc/apt/trusted.gpg.d/repoforge.public.key] \\ https://api.repoforge.io/debian/your-hash-id/ hello main\u0026#34; \\ \u0026gt; /etc/apt/sources.list.d/repoforge.list # store RepoForge.io access token on the target machine RUN echo \u0026#34;machine https://api.repoforge.io\\n \\ login __token__\\n \\ password ${REPOFORGE_ACCESS_TOKEN}\u0026#34; \u0026gt; /etc/apt/auth.conf.d/repoforge # update apt again then install your package RUN apt-get update RUN apt install hello -y CMD hello Let\u0026rsquo;s build the image, remembering to pass in our RepoForge access token:\ndocker build -t hello --build-arg REPOFORGE_ACCESS_TOKEN=your-access-token . And finally, we should be able to run our Dockerfile:\n$ docker run hello Hello, world! Your private Debian package is now hosted on RepoForge and can be installed on any machine with the correct credentials configured.\n","permalink":"https://repoforge.io/blog/posts/managing-debian-packages-in-a-private-apt-repository/","summary":"\u003cp\u003eThis tutorial covers:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eCreating a simple Debian package using fpm\u003c/li\u003e\n\u003cli\u003eUploading it to a private APT repository on RepoForge.io\u003c/li\u003e\n\u003cli\u003eInstalling it from RepoForge in a Docker container\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"step-1--create-a-debian-package\"\u003eStep 1 – Create a Debian package\u003c/h2\u003e\n\u003cp\u003eFor this tutorial, we are going to make a simple hello world application. First, let\u0026rsquo;s create a working directory called hello, and add a subfolder called usr inside it, then another subfolder called bin inside that. Then, we\u0026rsquo;re going to add a file called hello inside of the bin folder. Your folder structure should look like this:\u003c/p\u003e","title":"How to Create and Manage Debian Packages in a Private APT Repository"},{"content":"This tutorial shows you how to configure uv to install Python packages from a private PyPI server — specifically a hosted private Python package registry on RepoForge.io.\nWhat is uv? uv is an extremely fast Python package installer and resolver, written in Rust by Astral (the creators of Ruff). It\u0026rsquo;s a drop-in replacement for pip and pip-tools that\u0026rsquo;s 10-100x faster.\nPrerequisites A RepoForge.io account Your RepoForge repository URL (found under Show me how to publish packages) An access token with Python read permissions A package already published to your RepoForge repository Installing uv Install uv using the official installer:\ncurl -LsSf https://astral.sh/uv/install.sh | sh Or with pip:\npip install uv Verify the installation:\nuv --version Configuring Authentication uv supports the same authentication methods as pip. The recommended approach is to use an access token.\nOption 1: Environment Variable Set the UV_EXTRA_INDEX_URL environment variable with your credentials embedded:\nexport UV_EXTRA_INDEX_URL=https://__token__:your-access-token@api.repoforge.io/your-hash-id/ Option 2: pip.conf uv reads pip\u0026rsquo;s configuration file. Add your RepoForge repository to ~/.config/pip/pip.conf (Linux/macOS) or %APPDATA%\\pip\\pip.ini (Windows):\n[global] extra-index-url = https://__token__:your-access-token@api.repoforge.io/your-hash-id/ Option 3: pyproject.toml (per-project) For project-specific configuration, add the source to your pyproject.toml:\n[tool.uv] extra-index-url = [\u0026#34;https://__token__:your-access-token@api.repoforge.io/your-hash-id/\u0026#34;] Security note: Avoid committing credentials to version control. Use environment variables in CI/CD pipelines.\nInstalling Packages Once configured, install packages from your private repository:\nuv pip install my-private-package Or with a requirements file:\nuv pip install -r requirements.txt uv will automatically check both PyPI and your private RepoForge repository when resolving dependencies.\nUsing with uv Projects If you\u0026rsquo;re using uv\u0026rsquo;s project management features, add dependencies directly:\nuv add my-private-package This will update your pyproject.toml and uv.lock files.\nUsing in Docker For Docker builds, pass the credentials as a build argument:\nFROM python:3.12-slim ARG REPOFORGE_ACCESS_TOKEN RUN pip install uv ENV UV_EXTRA_INDEX_URL=https://__token__:${REPOFORGE_ACCESS_TOKEN}@api.repoforge.io/your-hash-id/ COPY requirements.txt . RUN uv pip install --system -r requirements.txt Build with:\ndocker build --build-arg REPOFORGE_ACCESS_TOKEN=your-access-token -t myapp . Using in CI/CD For GitHub Actions, store your access token as a secret and configure uv:\n- name: Install dependencies env: UV_EXTRA_INDEX_URL: https://__token__:${{ secrets.REPOFORGE_TOKEN }}@api.repoforge.io/your-hash-id/ run: | pip install uv uv pip install --system -r requirements.txt Troubleshooting 401 Unauthorized Verify your access token has Python read permissions Check the token hasn\u0026rsquo;t expired or been revoked Ensure you\u0026rsquo;re using __token__ as the username Package not found Confirm the package exists in your RepoForge repository Check the package name matches exactly (case-sensitive) Verify your repository URL is correct Your private Python packages can now be installed with uv\u0026rsquo;s speed and reliability.\n","permalink":"https://repoforge.io/blog/posts/installing-private-python-packages-with-uv/","summary":"\u003cp\u003eThis tutorial shows you how to configure \u003ca href=\"https://github.com/astral-sh/uv\"\u003euv\u003c/a\u003e to install Python packages from a \u003ca href=\"https://repoforge.io\"\u003eprivate PyPI server\u003c/a\u003e — specifically a hosted \u003ca href=\"https://repoforge.io/python\"\u003eprivate Python package registry\u003c/a\u003e on RepoForge.io.\u003c/p\u003e\n\u003ch2 id=\"what-is-uv\"\u003eWhat is uv?\u003c/h2\u003e\n\u003cp\u003euv is an extremely fast Python package installer and resolver, written in Rust by Astral (the creators of Ruff). It\u0026rsquo;s a drop-in replacement for pip and pip-tools that\u0026rsquo;s 10-100x faster.\u003c/p\u003e\n\u003ch2 id=\"prerequisites\"\u003ePrerequisites\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eA \u003ca href=\"https://repoforge.io\"\u003eRepoForge.io\u003c/a\u003e account\u003c/li\u003e\n\u003cli\u003eYour RepoForge repository URL (found under \u003cstrong\u003eShow me how to publish packages\u003c/strong\u003e)\u003c/li\u003e\n\u003cli\u003eAn access token with Python read permissions\u003c/li\u003e\n\u003cli\u003eA package already published to your RepoForge repository\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"installing-uv\"\u003eInstalling uv\u003c/h2\u003e\n\u003cp\u003eInstall uv using the official installer:\u003c/p\u003e","title":"How to Install Private Python Packages with uv"},{"content":"This tutorial shows how to automatically build and publish Python packages to a private repository using GitLab CI/CD. By the end, every tagged commit will trigger a pipeline that packages your code and uploads it to a private package registry for CI/CD on RepoForge.io.\nPrerequisites A RepoForge.io account (free trial works for this tutorial) A GitLab account A recent version of Python 3 Basic familiarity with git Step 1 — Create the folder structure Create an empty folder for your project and add a subfolder for your package code:\nmy_project/ my_package/ __init__.py ... your code goes here Step 2 — Create pyproject.toml Create the below pyproject.toml file in your my_project folder:\n[build-system] requires = [\u0026#34;setuptools\u0026gt;=61.0\u0026#34;, \u0026#34;wheel\u0026#34;] build-backend = \u0026#34;setuptools.build_meta\u0026#34; [project] name = \u0026#34;my_package\u0026#34; version = \u0026#34;0.1.0\u0026#34; description = \u0026#34;My Python package\u0026#34; authors = [ {name = \u0026#34;Your Name\u0026#34;, email = \u0026#34;you@example.com\u0026#34;} ] license = {text = \u0026#34;MIT\u0026#34;} requires-python = \u0026#34;\u0026gt;=3.9\u0026#34; [project.optional-dependencies] dev = [\u0026#34;twine\u0026#34;, \u0026#34;build\u0026#34;] Note: If you have existing code using setup.py, it will still work. However, pyproject.toml is now the standard and recommended approach.\nStep 3 — Build the package Install the build tools and create your distribution files:\npip install build twine python -m build This creates a dist/ folder containing your package:\nmy_project/ dist/ my_package-0.1.0.tar.gz my_package-0.1.0-py3-none-any.whl my_package/ __init__.py pyproject.toml Step 4 — Create an access token in RepoForge.io Before uploading, you need to create an access token for authentication.\nLog in to RepoForge.io. You\u0026rsquo;ll need to create an account if you haven\u0026rsquo;t already. Go to Access Tokens in the sidebar Click Create Access Token Give it a descriptive name (e.g., \u0026ldquo;GitLab CI\u0026rdquo;) Assign the role called Python - Full Access then click the Create Access Token button Copy the token value — you won\u0026rsquo;t be able to see it again Step 5 — Test the upload locally Next you\u0026rsquo;ll need to find your repository URL in the RepoForge.io dashboard. To do this, click on Python under Repositories at the top of the main left hand sidebar. Then click Show me how to publish packages in the top right hand corner.\nTest that everything works by uploading manually:\ntwine upload \\ --repository-url https://api.repoforge.io/your-unique-hash/ \\ -u __token__ \\ -p YOUR_ACCESS_TOKEN \\ dist/* You should see output like:\nUploading distributions to https://api.repoforge.io/your-unique-hash/ Uploading my_package-0.1.0-py3-none-any.whl 100%|████████████████████████| 3.85k/3.85k [00:00\u0026lt;00:00, 8.83kB/s] Uploading my_package-0.1.0.tar.gz 100%|████████████████████████| 3.38k/3.38k [00:00\u0026lt;00:00, 3.72kB/s] Refresh the RepoForge.io dashboard to confirm the package appears.\nStep 6 — Create a GitLab repository Create a new project at https://gitlab.com/projects/new, then initialise your local repository:\ngit init git remote add origin git@gitlab.com:your-username/my-project.git Add a .gitignore to exclude build artifacts:\ndist/ build/ *.egg-info/ __pycache__/ .venv/ Step 7 — Create the GitLab CI pipeline Create .gitlab-ci.yml in your project root:\nstages: - package publish: stage: package image: python:3.13-alpine only: - tags script: - pip install build twine - python -m build - twine upload --non-interactive dist/* The only: tags configuration means this job only runs when you push a git tag, not on every commit. The --non-interactive flag prevents twine from waiting for input in the CI environment.\nStep 8 — Configure GitLab CI/CD variables The pipeline needs credentials to upload to RepoForge.io. Twine reads these from environment variables automatically.\nIn GitLab, go to Settings → CI/CD → Variables and add:\nVariable Value TWINE_REPOSITORY_URL Your RepoForge.io Python repository URL TWINE_USERNAME __token__ TWINE_PASSWORD Your access token from Step 4 Mark TWINE_PASSWORD as masked to prevent it appearing in logs.\nStep 9 — Use git tags for versioning The pipeline only runs on tagged commits, and you should use these tags as version numbers. Update pyproject.toml to read the version dynamically:\n[build-system] requires = [\u0026#34;setuptools\u0026gt;=61.0\u0026#34;, \u0026#34;wheel\u0026#34;] build-backend = \u0026#34;setuptools.build_meta\u0026#34; [project] name = \u0026#34;my_package\u0026#34; dynamic = [\u0026#34;version\u0026#34;] description = \u0026#34;My Python package\u0026#34; authors = [ {name = \u0026#34;Your Name\u0026#34;, email = \u0026#34;you@example.com\u0026#34;} ] license = {text = \u0026#34;MIT\u0026#34;} requires-python = \u0026#34;\u0026gt;=3.9\u0026#34; [tool.setuptools.dynamic] version = {attr = \u0026#34;my_package.__version__\u0026#34;} Then set the version in your package\u0026rsquo;s __init__.py:\n__version__ = \u0026#34;0.2.0\u0026#34; Alternatively, for CI environments, you can use the CI_COMMIT_TAG environment variable. Update .gitlab-ci.yml:\nstages: - package publish: stage: package image: python:3.13-alpine only: - tags script: - pip install build twine - sed -i \u0026#34;s/version = \\\u0026#34;.*\\\u0026#34;/version = \\\u0026#34;$CI_COMMIT_TAG\\\u0026#34;/\u0026#34; pyproject.toml - python -m build - twine upload --non-interactive dist/* Step 10 — Push and deploy Commit your changes, create a tag, and push:\ngit add . git commit -m \u0026#34;Initial commit\u0026#34; git tag 0.2.0 git push origin main --tags The pipeline will trigger automatically. Check the GitLab CI/CD → Pipelines page to monitor progress. Once complete, the new version appears in RepoForge.io.\nTroubleshooting \u0026ldquo;Conflict for URL\u0026rdquo; error RepoForge.io (like PyPI) doesn\u0026rsquo;t allow uploading the same version twice. Either:\nDelete the existing version in the dashboard and re-upload Increment the version number and push a new tag Pipeline hangs waiting for input Add --non-interactive to the twine command, or set the environment variable:\nvariables: TWINE_NON_INTERACTIVE: \u0026#34;1\u0026#34; \u0026ldquo;403 Forbidden\u0026rdquo; or authentication errors Verify TWINE_USERNAME is set to __token__ (not your email) Check the access token has a role with Python write permissions Ensure the token hasn\u0026rsquo;t been rotated (invalidating the old value) Package not found after upload Check you\u0026rsquo;re looking at the correct RepoForge.io account Verify the upload completed successfully in the pipeline logs Try refreshing the dashboard Next steps Add tests to your pipeline before the publish stage Configure branch protection to control who can create tags ","permalink":"https://repoforge.io/blog/posts/building-and-publishing-python-packages-in-your-gitlab-ci-pipeline/","summary":"\u003cp\u003eThis tutorial shows how to automatically build and publish Python packages to a private repository using GitLab CI/CD. By the end, every tagged commit will trigger a pipeline that packages your code and uploads it to a \u003ca href=\"https://repoforge.io\"\u003eprivate package registry for CI/CD\u003c/a\u003e on RepoForge.io.\u003c/p\u003e\n\u003ch2 id=\"prerequisites\"\u003ePrerequisites\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eA \u003ca href=\"https://repoforge.io\"\u003eRepoForge.io\u003c/a\u003e account (free trial works for this tutorial)\u003c/li\u003e\n\u003cli\u003eA \u003ca href=\"https://gitlab.com\"\u003eGitLab\u003c/a\u003e account\u003c/li\u003e\n\u003cli\u003eA recent version of Python 3\u003c/li\u003e\n\u003cli\u003eBasic familiarity with git\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"step-1--create-the-folder-structure\"\u003eStep 1 — Create the folder structure\u003c/h2\u003e\n\u003cp\u003eCreate an empty folder for your project and add a subfolder for your package code:\u003c/p\u003e","title":"How to Publish Python Packages to a Private PyPI from GitLab CI/CD"},{"content":"This tutorial shows you how to publish Python packages built with Poetry to a private PyPI server — a hosted private Python package registry on RepoForge.io.\nPrerequisites A RepoForge.io account Your RepoForge repository URL (found under Show me how to publish packages) An access token with Python write permissions Installing Poetry Install Poetry using the official installer:\ncurl -sSL https://install.python-poetry.org | python3 - Verify the installation:\npoetry --version Creating a Package in Poetry Now that poetry is installed you can create a poetry project like this:\npoetry new private-package This will create a subfolder called private-package, containing a package skeleton.\nAnd that\u0026rsquo;s it! We have created an empty Python package. We\u0026rsquo;re going to leave it blank for the sake of this simple tutorial.\nPointing Poetry at Your Private Repository We can now configure Poetry to publish our private-package package to our RepoForge private repository by using the below command, replacing the URL with our unique RepoForge repo URL:\npoetry config repositories.repoforge https://api.repoforge.io/abcdef Next, configure your RepoForge access token. Use __token__ as the username and your access token as the password:\npoetry config http-basic.repoforge __token__ your-access-token Publishing the Package to Our Private Repository First, build your package:\npoetry build Then publish to RepoForge:\npoetry publish -r repoforge If all goes well, your private Python package will be uploaded successfully. You can confirm this by refreshing your RepoForge UI:\nInstalling a Private Package with Poetry Now that we have published our Python package to a private repository, we can install it in other poetry projects. To do this, you\u0026rsquo;ll need to add this section to your pyproject.toml (inserting your private repository URL):\n[[tool.poetry.source]] name = \u0026#34;RepoForge\u0026#34; url = \u0026#34;https://api.repoforge.io/abcdef\u0026#34; You can now do the following to install python packages locally:\npoetry add private-package --source repoforge Your private Python package is now published and can be installed in any Poetry project with the source configured.\n","permalink":"https://repoforge.io/blog/posts/publishing-python-packages-with-poetry/","summary":"\u003cp\u003eThis tutorial shows you how to publish Python packages built with Poetry to a \u003ca href=\"https://repoforge.io\"\u003eprivate PyPI server\u003c/a\u003e — a hosted \u003ca href=\"https://repoforge.io/python\"\u003eprivate Python package registry\u003c/a\u003e on RepoForge.io.\u003c/p\u003e\n\u003ch2 id=\"prerequisites\"\u003ePrerequisites\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eA \u003ca href=\"https://repoforge.io\"\u003eRepoForge.io\u003c/a\u003e account\u003c/li\u003e\n\u003cli\u003eYour RepoForge repository URL (found under \u003cstrong\u003eShow me how to publish packages\u003c/strong\u003e)\u003c/li\u003e\n\u003cli\u003eAn access token with Python write permissions\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"installing-poetry\"\u003eInstalling Poetry\u003c/h2\u003e\n\u003cp\u003eInstall Poetry using the official installer:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -sSL https://install.python-poetry.org | python3 -\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eVerify the installation:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epoetry --version\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"creating-a-package-in-poetry\"\u003eCreating a Package in Poetry\u003c/h2\u003e\n\u003cp\u003eNow that poetry is installed you can create a poetry project like this:\u003c/p\u003e","title":"How to Publish Python Packages to a Private Repository with Poetry"},{"content":"This tutorial shows you how to automatically publish Python, Docker, and NPM packages to a private package registry for CI/CD on RepoForge.io using the official GitHub Action.\nStep 1: Create a GitHub repository You can refer to the example here to see a valid repository setup that works with the options described below.\nStep 2: Get Your RepoForge.io Credentials Log into your RepoForge.io account and:\nFind your Hash ID on your account dashboard. (You will use this in package URLs.)\nCreate an access token in the RepoForge dashboard under Access Tokens, ensuring it has write permissions enabled for the package types you plan to publish (Python, Docker, or NPM).\nStep 3: Store Credentials Securely in GitHub In your GitHub repository, navigate to Settings → Secrets and variables → Actions and create these secrets:\nREPOFORGE_TOKEN: Your RepoForge.io access token REPOFORGE_HASH_ID: Your RepoForge.io hash ID Step 4: Create Your GitHub Actions Workflow Create a new file .github/workflows/publish.yml in your repository and insert the following content:\nname: Publish Python Package to RepoForge on: push: branches: [ main ] jobs: publish-python: runs-on: ubuntu-latest steps: - name: Checkout repo uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: \u0026#39;3.13\u0026#39; - name: Install build tools run: pip install build - name: Build package run: python -m build - name: Publish to RepoForge uses: RepoForge-io/repoforge-publish-action@main with: api_token: ${{ secrets.REPOFORGE_TOKEN }} hash_id: ${{ secrets.REPOFORGE_HASH_ID }} publish-docker: runs-on: ubuntu-latest name: Build and push Docker image steps: - name: Checkout repo uses: actions/checkout@v4 - name: Publish Docker image to RepoForge uses: RepoForge-io/repoforge-publish-action@main with: package_type: docker api_token: ${{ secrets.REPOFORGE_TOKEN }} hash_id: ${{ secrets.REPOFORGE_HASH_ID }} registry_name: my-registry docker_context: docker publish-npm: runs-on: ubuntu-latest name: Publish NPM package steps: - name: Checkout repo uses: actions/checkout@v4 - name: Publish NPM package uses: RepoForge-io/repoforge-publish-action@main with: package_type: npm api_token: ${{ secrets.REPOFORGE_TOKEN }} hash_id: ${{ secrets.REPOFORGE_HASH_ID }} package_dir: npm Step 5: Push your code and watch the pipeline run Once you\u0026rsquo;ve configured your actions correctly, just commit and push the code to your repository. You should see three green blobs if everything works correctly:\nStep 6: Check the outputs in your RepoForge.io dashboard If your Github Actions passed successfully, you should be able to see your published artifacts in RepoForge.io:\nComing Soon: Conda and Debian The GitHub Action will soon support Conda and Debian packages, enabling even broader integration for your CI/CD workflows.\nConclusion Automating package publishing saves you valuable time, streamlines deployments, and reduces errors. With RepoForge.io\u0026rsquo;s official GitHub Action, integration is simple and effective, giving your team more space to focus on building great software.\n","permalink":"https://repoforge.io/blog/posts/publishing-packages-with-github-actions/","summary":"\u003cp\u003eThis tutorial shows you how to automatically publish Python, Docker, and NPM packages to a \u003ca href=\"https://repoforge.io\"\u003eprivate package registry for CI/CD\u003c/a\u003e on RepoForge.io using the \u003ca href=\"https://github.com/marketplace/actions/repoforge-publish\"\u003eofficial GitHub Action\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"step-1-create-a-github-repository\"\u003eStep 1: Create a GitHub repository\u003c/h2\u003e\n\u003cp\u003eYou can refer to the \u003ca href=\"https://github.com/RepoForge-io/repoforge-publish-action\"\u003eexample here\u003c/a\u003e to see a valid repository setup that works with the options described below.\u003c/p\u003e\n\u003ch3 id=\"step-2-get-your-repoforgeio-credentials\"\u003eStep 2: Get Your RepoForge.io Credentials\u003c/h3\u003e\n\u003cp\u003eLog into your RepoForge.io account and:\u003c/p\u003e\n\u003cp\u003eFind your Hash ID on your account dashboard. (You will use this in package URLs.)\u003c/p\u003e","title":"How to Publish Python, Docker, and NPM Packages with GitHub Actions"}]