In the previous tutorial, we mastered strings and regular expressions. Now let’s learn how to organize your code into modules and packages, and how to manage dependencies with virtual environments.
This is where Python goes from writing scripts to building real projects. By the end of this tutorial, you will know how to import code, structure a project, install packages, and use modern tools like pyproject.toml and uv.
What Is a Module?
A module is simply a Python file. When you create a file called math_utils.py, you create a module called math_utils.
# math_utils.py
def add(a: int, b: int) -> int:
return a + b
def multiply(a: int, b: int) -> int:
return a * b
Now you can import and use it from another file:
# main.py
import math_utils
result = math_utils.add(3, 4)
print(result) # 7
That is it. Every .py file is a module.
Import Styles
Python has several ways to import code. Each has its place.
import module
import json
data = json.dumps({"name": "Alex"})
This imports the entire module. You access functions with module.function(). This is the safest style because it is clear where each function comes from.
from module import name
from datetime import datetime, timedelta
now = datetime.now()
tomorrow = now + timedelta(days=1)
This imports specific names directly. You do not need the module prefix. Use this when you use specific functions frequently.
import with alias
from datetime import datetime as dt
now = dt.now()
Aliases shorten long module names. Common conventions: import numpy as np, import pandas as pd.
from module import *
from math import * # Imports everything — avoid this!
This imports all names from a module. Do not use this. It clutters your namespace and makes it hard to know where a function came from.
The __name__ == "__main__" Guard
This is one of the most important Python patterns:
# math_utils.py
def add(a: int, b: int) -> int:
return a + b
if __name__ == "__main__":
# This only runs when you execute math_utils.py directly
print(add(3, 4)) # 7
When you run a file directly (python math_utils.py), Python sets __name__ to "__main__". When you import the file, __name__ is set to the module name (like "math_utils").
This guard lets you:
- Add test code that runs when you execute the file
- Prevent that test code from running when the file is imported
You should use this guard in every file that has runnable code.
What Is a Package?
A package is a directory that contains Python modules. It needs an __init__.py file (which can be empty):
my_project/
__init__.py
math_utils.py
string_utils.py
Now you can import from the package:
from my_project.math_utils import add
from my_project.string_utils import slugify
The __init__.py File
__init__.py runs when the package is imported. You can use it to expose the package’s public API:
# my_project/__init__.py
from .math_utils import add, multiply
from .string_utils import slugify
Now users can import directly from the package:
from my_project import add, slugify
The __init__.py file can be empty. But using it to define your public API makes imports cleaner.
Relative Imports
Inside a package, you can use relative imports:
# my_project/string_utils.py
from .math_utils import add # Same package (one dot)
from ..other_package import helper # Parent package (two dots)
Relative imports only work inside packages. They do not work in standalone scripts.
Standard Library Highlights
Python comes with a huge standard library. Here are the most useful modules:
os and sys — System Access
import os
import sys
print(os.getcwd()) # Current working directory
print(sys.platform) # 'darwin', 'linux', or 'win32'
print(sys.version) # Python version string
pathlib — Modern File Paths
from pathlib import Path
current = Path(".")
home = Path.home()
config = home / ".config" / "myapp" # Build paths with /
# Check if a file exists
if Path("README.md").exists():
print("README found")
pathlib is the modern way to work with file paths. It is cleaner than os.path.
datetime — Dates and Times
from datetime import datetime, timedelta
now = datetime.now()
print(now.strftime("%Y-%m-%d %H:%M:%S"))
tomorrow = now + timedelta(days=1)
print(tomorrow.strftime("%A, %B %d")) # "Wednesday, June 18"
json — JSON Data
import json
# Python dict to JSON string
data = {"name": "Alex", "age": 25}
json_str = json.dumps(data, indent=2)
# JSON string to Python dict
parsed = json.loads(json_str)
collections — Useful Data Structures
from collections import Counter, defaultdict
# Count occurrences
counter = Counter("hello world")
print(counter.most_common(3))
# Dict with default values
groups = defaultdict(list)
groups["fruits"].append("apple")
Project Structure
For anything bigger than a single file, you need a proper project structure:
my-project/
src/
my_project/
__init__.py
main.py
utils.py
tests/
test_main.py
test_utils.py
.gitignore
pyproject.toml
README.md
This is the src layout, recommended for modern Python projects. Your source code lives in src/, tests in tests/, and configuration in pyproject.toml.
Virtual Environments
Why Virtual Environments?
Imagine you have two projects:
- Project A needs
requestsversion 2.28 - Project B needs
requestsversion 2.31
If you install packages globally, one project will break. Virtual environments solve this. Each project gets its own isolated set of packages.
Creating a Virtual Environment
# Create a virtual environment called .venv
python3 -m venv .venv
# Activate it (macOS/Linux)
source .venv/bin/activate
# Activate it (Windows)
.venv\Scripts\activate
After activation, your terminal prompt changes to show (.venv). Now pip install installs packages only for this project.
Deactivating
deactivate
This returns you to the global Python environment.
Installing Packages with pip
pip is Python’s package installer:
# Install a package
pip install requests
# Install a specific version
pip install requests==2.31.0
# Install multiple packages
pip install requests pydantic fastapi
# Upgrade a package
pip install --upgrade requests
# Uninstall
pip uninstall requests
requirements.txt
Save your project’s dependencies in requirements.txt:
# Generate from current environment
pip freeze > requirements.txt
# Install from requirements.txt
pip install -r requirements.txt
A requirements.txt file looks like this:
requests==2.31.0
pydantic==2.5.0
pytest==8.0.0
This makes it easy for others to install the same packages.
Modern Python: pyproject.toml
requirements.txt works, but pyproject.toml is the modern standard. It replaces setup.py, setup.cfg, and requirements.txt:
[project]
name = "my-project"
version = "1.0.0"
description = "A sample Python project"
requires-python = ">=3.12"
dependencies = [
"requests>=2.31.0",
"pydantic>=2.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"ruff>=0.5.0",
]
[tool.ruff]
line-length = 88
target-version = "py312"
pyproject.toml is:
- One file for all project configuration
- The official standard (PEP 621)
- Tool configuration included (ruff, pytest, etc.)
Modern Python: uv
uv is a new package manager that is 10-100x faster than pip. It was created by the team behind ruff.
Install uv
# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Or with pip
pip install uv
Using uv
# Create a virtual environment
uv venv
# Install packages (10x faster than pip)
uv pip install requests pydantic
# Install from requirements.txt
uv pip install -r requirements.txt
# Install from pyproject.toml
uv pip install -e ".[dev]"
# Sync dependencies (install exactly what's specified)
uv pip sync requirements.txt
uv is a drop-in replacement for pip. The commands are almost identical, just prefix with uv. As of 2026, uv is becoming the standard tool for Python dependency management.
uv Can Also Install Python
# Install a specific Python version
uv python install 3.13
# Create a project with uv
uv init my-project
cd my-project
uv add requests pydantic
.gitignore for Python
Every Python project needs a .gitignore file:
# Virtual environment
.venv/
venv/
# Python cache
__pycache__/
*.pyc
*.pyo
# Distribution
dist/
build/
*.egg-info/
# IDE
.vscode/
.idea/
# Environment variables
.env
# Ruff cache
.ruff_cache/
# Pytest cache
.pytest_cache/
Never commit your virtual environment or __pycache__ to git. They are large, system-specific, and can be recreated.
Brief Mention: conda
You may have heard of conda (or Anaconda/Miniconda). It is a package manager and environment manager popular in data science.
conda manages both Python packages and non-Python dependencies (like C libraries). This makes it useful for data science, where packages like NumPy and SciPy depend on compiled code.
However, for general Python development, uv is the recommended choice in 2026. It is faster, simpler, and follows Python standards more closely. Use conda only if you work in data science and need it for specific packages.
Practical Example: Project Setup
Let me walk through setting up a new Python project from scratch:
# Create project directory
mkdir my-project
cd my-project
# Create virtual environment
python3 -m venv .venv
source .venv/bin/activate
# Create project structure
mkdir -p src/my_project tests
touch src/my_project/__init__.py
touch tests/__init__.py
# Initialize git
git init
echo ".venv/" > .gitignore
echo "__pycache__/" >> .gitignore
# Install development tools
pip install pytest ruff
# Save dependencies
pip freeze > requirements.txt
Or with uv (faster):
mkdir my-project
cd my-project
uv venv
source .venv/bin/activate
uv pip install pytest ruff
This is the workflow you will use for every new project. It takes about 30 seconds.
The import Search Path
When you write import my_module, Python searches for the module in this order:
- Current directory — the directory where your script is
- PYTHONPATH — directories in the PYTHONPATH environment variable
- Standard library — Python’s built-in modules
- Site packages — installed third-party packages
You can see the full search path with:
import sys
for path in sys.path:
print(path)
If Python cannot find your module, check that the file is in one of these locations.
Common Mistakes
Installing Packages Globally
# BAD: installs globally
pip install requests
# GOOD: activate venv first
source .venv/bin/activate
pip install requests
Always use a virtual environment. Global installs can break your system Python.
Circular Imports
# module_a.py
from module_b import func_b # Imports module_b
# module_b.py
from module_a import func_a # Imports module_a — CIRCULAR!
This causes an ImportError. Fix it by:
- Moving shared code to a third module
- Using lazy imports (import inside the function that needs it)
Shadowing Standard Library Names
# BAD: do NOT name your file json.py, os.py, or sys.py
# It shadows the standard library module!
If you create a file called json.py, Python will import your file instead of the standard json module. Choose unique names for your files.
Forgetting to Activate the Virtual Environment
If pip install does not seem to work, check if your virtual environment is active. Look for (.venv) in your terminal prompt.
Summary
Here is a quick reference for modules, packages, and environments:
| Concept | Purpose | Example |
|---|---|---|
| Module | A single .py file | import math_utils |
| Package | A directory with __init__.py | from my_project import add |
__name__ guard | Run code only when executed directly | if __name__ == "__main__": |
| Virtual env | Isolated package environment | python3 -m venv .venv |
| pip | Install packages | pip install requests |
| requirements.txt | List dependencies | pip freeze > requirements.txt |
| pyproject.toml | Modern project config | Standard since PEP 621 |
| uv | Fast package manager | uv pip install requests |
The key takeaway: always use a virtual environment and define your dependencies in requirements.txt or pyproject.toml. This makes your project reproducible and shareable.
Source Code
You can find the code for this tutorial on GitHub:
kemalcodes/python-tutorial — tutorial-08-modules
Run the examples:
python src/py08_modules.py
Run the tests:
python -m pytest tests/test_py08.py -v
What’s Next?
Congratulations! You have completed the Foundations section of this Python tutorial series. You now know variables, control flow, functions, data structures, strings, modules, and virtual environments.
In the next section, we move to intermediate Python: classes, dataclasses, error handling, file I/O, and more. The next tutorial covers Object-Oriented Programming with classes and objects.
Related Articles
- Python Tutorial #7: Strings — string methods, formatting, regex
- Python Tutorial #6: Data Structures — lists, dicts, sets, tuples
- Python Cheat Sheet — quick reference for Python syntax