
Note: This article was originally written in 2019. Tooling and package recommendations in this post have been revised afterwards to better reflect current Python practices and libraries.
Introduction Link to heading
Welcome to the final instalment of the series! Throughout the previous articles we have worked through the core of the Python language itself, from basic syntax and OOP all the way through collections, inheritance, magic methods, and introspection. In this closing article, as promised back in part one, we are approaching the broader Python ecosystem, covering package management, virtual environments and the tooling landscape.
If you are coming from C# and .NET there’s nothing ground breaking here; the problems being solved are largely the same. The philosophy, however, tends to favour lighter, composable tools over a single monolithic environment.
pip: Python’s Package Manager Link to heading
In the .NET world, NuGet is the package manager of choice: you reference a package in your project file, NuGet resolves and downloads the dependency, and the compiler picks it up. Python’s equivalent is pip, a command-line tool that ships with modern Python installations out of the box (Python 3.4+). Which means, you normally do not need to install a separate package manager first. Install Python, and pip will already be there.
Installing a package is as simple as this:
pip install requestsThis downloads the requests library, the de facto standard for making HTTP requests in Python, and makes it available to import in your code. You can install a specific version, a version range, or upgrade an existing package:
pip install requests==2.28.0 # specific version
pip install "requests>=2.25,<3" # version range
pip install --upgrade requests # upgrade to latestThe equivalent of a NuGet package reference list in Python is a requirements file, by convention named requirements.txt:
requests==2.28.0
flask==2.3.2
sqlalchemy==2.0.15You can generate this file automatically from your currently installed packages with:
pip freeze > requirements.txtAnd restore all dependencies from it, say, when setting up the project on a new machine, with:
pip install -r requirements.txtThis is where the parallel with dotnet restore is probably strongest. The default scope is what makes a real difference: pip by default installs packages globally into your Python installation, unlike the .NET resolution scoped per-project. In Python, without any further setup, installing two projects’ dependencies globally will quickly lead to version clashes. This is where virtual environments come in.
Virtual Environments Link to heading
The solution is a virtual environment: an isolated Python installation scoped to a single project, with its own installed packages entirely separate from the global Python and distinct from other project. Since Python 3.3, the standard way to create one is with venv, which is again part of the standard library:
# Create the virtual environment in your project folder (run once)
python -m venv .venv
# Activate it (macOS/Linux)
source .venv/bin/activate
# Activate it (Windows)
.venv\Scripts\activateOnce activated, any pip install commands will install packages into the virtual environment rather than the global Python installation, and python will refer to the interpreter inside the environment. Your shell prompt will typically change to show the active environment name, so it is easy to tell at a glance whether you are inside one. To leave the environment, run deactivate.
It is good practice to add your .venv folder to .gitignore and commit only the requirements.txt, much as you would not commit bin/ or obj/ folders in a .NET project. You can always regenerate your venv as needed!
uv: A Modern Alternative Workflow Link to heading
uv is a more modern tool that combines much of what the previous tools offer. It is a fast Python package and environment manager written in Rust, and it aims to replace multiple tools and command with a single interface.
If working with a legacy project looks a bit like this:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtThe uv alternative will be:
uv venv
uv syncOr, when starting a fresh project:
uv init
uv add requests
uv run python app.pyThe main problems uv tries to solve are practical rather than conceptual:
- Speed: package resolution and installation are usually much faster than pip-based workflows.
- Fewer moving parts: one tool handles environment creation, dependency installation, locking, and command execution.
- Reproducibility: uv encourages lock file installs, reducing drift.
- Cleaner project UX: commands like
uv add,uv sync, anduv runmap directly to common development tasks.
A sensible approach for most teams is to learn the built-in baseline first (pip + venv), then adopt uv once everyone is comfortable with the fundamentals. That way you get the convenience benefits without losing sight of what is happening under the hood.
Popular Packages Worth Knowing Link to heading
The Python ecosystem is quite large, and one of its genuine strengths is the breadth of available packages. As a C# developer venturing in, a handful of libraries will come up quickly regardless of what you are building:
- requests: HTTP for humans. Clean, simple API for making web requests. For anything more complex than basic
urllibcalls, this is almost always the right choice. - FastAPI: A high-performance web API framework with first-class type-hint support, automatic OpenAPI docs, and a neat developer experience.
- Flask and Django: Flask remains a great lightweight option, while Django is the classic “batteries included” framework for larger full-stack applications. Both have a long history and strong support!
- Pydantic: Data validation and parsing with type hints. Especially useful for API payloads, configuration, and boundaries between layers.
- httpx: A modern HTTP client with both sync and async APIs, commonly used in newer FastAPI-style codebases.
- NumPy and pandas: If there is any data work in your future, these two are essentially unavoidable. NumPy provides efficient n-dimensional array operations; pandas builds on it to offer DataFrame-style data manipulation and analysis.
- pytest: The de facto standard for testing in Python, considerably more flexible than the built-in
unittestmodule. The closest equivalent to NUnit or xUnit in the .NET world.
Development Tools and IDEs Link to heading
The most popular choices for a Python IDE are:
- PyCharm (JetBrains): The closest thing to a full-featured Python IDE. Excellent code intelligence, debugging, refactoring, and virtual environment management. The Community edition is free; the Professional edition adds web framework support and database tools. If you are familiar with Rider or IntelliJ, you will likely find it familiar.
- Visual Studio Code: With the Python extension, VS Code provides strong IntelliSense, debugging, and Jupyter notebook support. Considerably lighter than PyCharm and a natural choice if you are already using VS Code for other work.
Beyond the IDE itself, a Python project will typically involve a small collection of standalone tools:
- Ruff: The current default choice for many teams. It is extremely fast and can handle linting (and, increasingly, formatting) in one tool.
- Black: Still widely used as a dedicated formatter, especially in established projects that already standardised on it.
- mypy: A static type checker. Python 3 supports optional type hints, and mypy can verify them statically without running the code. This is well worth knowing about if you find Python’s dynamic typing a little unsettling coming from C#:
def add(a: int, b: int) -> int:
return a + b
add(1, 2) # Fine
add("x", "y") # mypy will flag this as a type errorType hints are entirely optional and have no effect on runtime behaviour; Python remains dynamically typed, but they can be invaluable in larger codebases for documentation, tooling support, and catching bugs early. Many popular libraries now ship with type stubs or inline annotations, so you get IDE autocompletion even for third-party code.
And That’s a Wrap Link to heading
That brings us to the end of the series! We’ve covered quite a lot of ground, from Python’s basic syntax and its differences with C#, through OOP, collections, inheritance, magic methods, introspection, and now the overall ecosystem. I hope that the series has given C# developers a solid and practical starting point for Python, with enough shared vocabulary to make the transition easier, and more fun.
Python continues to grow in relevance across a wide range of domains, from web development and automation to data science and machine learning. Its commitment to readability makes it one of the most approachable languages to pick up as a second or third language.