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 requests version 2.28
  • Project B needs requests version 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:

  1. Current directory — the directory where your script is
  2. PYTHONPATH — directories in the PYTHONPATH environment variable
  3. Standard library — Python’s built-in modules
  4. 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:

ConceptPurposeExample
ModuleA single .py fileimport math_utils
PackageA directory with __init__.pyfrom my_project import add
__name__ guardRun code only when executed directlyif __name__ == "__main__":
Virtual envIsolated package environmentpython3 -m venv .venv
pipInstall packagespip install requests
requirements.txtList dependenciespip freeze > requirements.txt
pyproject.tomlModern project configStandard since PEP 621
uvFast package manageruv 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.