DEV Community

Deepika Pusala
Deepika Pusala

Posted on

Python Packaging, Type Hints & Testing with pytest: A Complete Beginner Guide πŸ§ͺ

πŸ“¦ Packaging, Type Hints & Testing in Python: Let's Learn from ZERO!

If you're building a FastAPI + PostgreSQL backend (like a social-feed app), these topics are not random. They work together to make your project:

  • πŸ’» Easy to install and run on another computer
  • πŸ“š Organized with the correct dependencies
  • πŸ›‘οΈ Safer through type checking
  • βœ… Reliable through automated testing
  • 🧹 Easier to maintain as it grows

We'll cover the meaning, why we need each tool, how it works, commands, code examples, and how it applies to a real backend. πŸš€

πŸ—ΊοΈ The Big Picture

  1. πŸ“¦ Packaging & Dependency Management β†’ prepare the project and its libraries so it runs consistently
  2. 🏷️ Type Hints & Static Checking β†’ say what types your functions expect, and catch mistakes before running
  3. πŸ§ͺ Automated Testing with pytest β†’ check your code behaves correctly in different situations

πŸ“¦ PART 1: Virtual Environments (venv / Poetry)

1. πŸ€” What is a Python dependency?

Your social-feed backend needs things like:

  • ⚑ FastAPI: to create APIs
  • πŸ¦„ Uvicorn: to run the app
  • πŸ—„οΈ SQLAlchemy: to talk to the database
  • βœ”οΈ Pydantic: to validate data
  • πŸ”„ Alembic: for database migrations
  • πŸ§ͺ pytest: to test your code

You didn't write these yourself. You install and use them.

πŸ“Œ A dependency is an external package or library that your project needs in order to work.

from fastapi import FastAPI

app = FastAPI()
Enter fullscreen mode Exit fullscreen mode

Here, FastAPI is a dependency.

2. 🏠 What problem do virtual environments solve?

Imagine two projects:

Project FastAPI SQLAlchemy Pydantic
πŸ…°οΈ Social Feed 0.115 2.x 2.x
πŸ…±οΈ Old College Project 0.95 1.4 1.x

Both need FastAPI, but different versions. If you install everything globally, installing a version for Project B can break Project A. πŸ’₯

A virtual environment is an isolated Python environment created for one project. Think of separate rooms in a house 🏠: each project has its own room and its own libraries, so they never interfere.

3. πŸ› οΈ What is venv?

venv is Python's built-in module for creating virtual environments. No separate install needed.

Step 1: create a project folder

mkdir social-feed
cd social-feed
Enter fullscreen mode Exit fullscreen mode

Step 2: create the virtual environment

python -m venv .venv
Enter fullscreen mode Exit fullscreen mode
Part Meaning
python Run Python
-m Execute a Python module
venv Use the virtual environment module
.venv Name of the environment folder

Step 3: activate it (tells your terminal to use this environment's Python and packages)

Windows PowerShell:

.venv\Scripts\Activate.ps1
Enter fullscreen mode Exit fullscreen mode

Windows Command Prompt:

.venv\Scripts\activate.bat
Enter fullscreen mode Exit fullscreen mode

Linux / macOS:

source .venv/bin/activate
Enter fullscreen mode Exit fullscreen mode

You'll see (.venv) in your terminal prompt. βœ… That means it's active.

Step 4: install dependencies

pip install fastapi uvicorn sqlalchemy pytest
Enter fullscreen mode Exit fullscreen mode

pip is Python's package installer. It installs into the currently active environment.

Step 5: check what's installed

pip list
Enter fullscreen mode Exit fullscreen mode

Step 6: deactivate

deactivate
Enter fullscreen mode Exit fullscreen mode

4. πŸ“ pip freeze and requirements.txt

Your teammate downloads your project from GitHub. Will they automatically have your packages? No! Source code and installed libraries are separate things. You must tell them which packages and versions to install.

pip freeze
Enter fullscreen mode Exit fullscreen mode

Example output:

fastapi==0.115.0
uvicorn==0.30.6
sqlalchemy==2.0.35
pytest==8.3.3
Enter fullscreen mode Exit fullscreen mode

Save it to a file:

pip freeze > requirements.txt
Enter fullscreen mode Exit fullscreen mode

Your project now looks like:

social-feed/
β”‚
β”œβ”€β”€ .venv/
β”œβ”€β”€ app/
β”œβ”€β”€ requirements.txt
└── main.py
Enter fullscreen mode Exit fullscreen mode

Your teammate recreates the environment:

python -m venv .venv
# activate it, then:
pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

-r means read the requirements from a file.

⚠️ Important: Do not commit your .venv folder to GitHub. It's machine-specific and can be recreated. Add .venv/ to .gitignore.

5. 🎭 What is Poetry?

  • venv β†’ creates isolated environments
  • pip β†’ installs packages
  • requirements.txt β†’ records dependencies

Managing a big project with several separate tools and files gets messy. Poetry is a Python dependency and packaging tool that brings many of these tasks together. It can:

  • πŸ“š Manage dependencies
  • 🏠 Create and manage virtual environments
  • πŸ”’ Lock exact dependency versions
  • πŸ“¦ Build Python packages
  • πŸš€ Help publish packages

🧰 How do we use Poetry?

Install Poetry using its official installation instructions, then inside your project:

poetry init                          # creates pyproject.toml interactively
poetry new myproject                 # or start a brand-new project
poetry add fastapi sqlalchemy        # add dependencies
poetry add --group dev pytest mypy   # add development-only dependencies
poetry install                       # install everything defined in the project
poetry run python main.py            # run a command inside Poetry's environment
Enter fullscreen mode Exit fullscreen mode

πŸ“ Depending on your Poetry version and installed plugins, poetry shell may not be available. poetry run ... always works.

πŸ”’ What is poetry.lock?

When you install FastAPI, FastAPI itself depends on other packages. These are called transitive dependencies:

Your Project
    β”‚
    └── FastAPI
          β”‚
          β”œβ”€β”€ Starlette
          └── Other dependencies
Enter fullscreen mode Exit fullscreen mode

poetry.lock records the exact resolved versions of everything, including transitive dependencies, so everyone installs the same set. 🎯

6. βš–οΈ venv vs pip vs Poetry

Tool Main responsibility
venv Creates isolated environments
pip Installs Python packages
requirements.txt Lists dependencies
Poetry Manages dependencies, environments and packaging
pyproject.toml Stores project configuration and metadata
poetry.lock Locks resolved dependency versions

πŸ“Œ Poetry does not replace Python. It manages your project's packaging and dependencies. You can also use venv + pip without Poetry.


βš™οΈ PART 2: pyproject.toml & Dependency Management

7. πŸ“„ What exactly is pyproject.toml?

One of the most important files in modern Python projects. It's a configuration file used by packaging tools and other development tools, written in TOML (Tom's Obvious Minimal Language, just a human-readable config format).

name = "social-feed"
version = "1.0.0"
description = "A social media backend"
Enter fullscreen mode Exit fullscreen mode

🀷 Why do we need it?

Your project may use Poetry (dependencies), Ruff (linting), mypy (type checking), pytest (testing), and a build backend. Instead of scattering configs across many files, modern projects centralize much of it in pyproject.toml.

πŸ—‚οΈ Example structure

social-feed/
β”‚
β”œβ”€β”€ pyproject.toml
β”œβ”€β”€ poetry.lock
β”œβ”€β”€ README.md
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ main.py
β”‚   β”œβ”€β”€ models/
β”‚   β”œβ”€β”€ services/
β”‚   └── repositories/
β”‚
└── tests/
Enter fullscreen mode Exit fullscreen mode

🎭 Example pyproject.toml (Poetry format)

[tool.poetry]
name = "social-feed"
version = "1.0.0"
description = "Social Feed Backend"
authors = ["Developer"]

[tool.poetry.dependencies]
python = "^3.11"
fastapi = "^0.115"
sqlalchemy = "^2.0"
uvicorn = "^0.30"

[tool.poetry.group.dev.dependencies]
pytest = "^8.0"
mypy = "^1.0"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Enter fullscreen mode Exit fullscreen mode
Section Purpose
[tool.poetry] Project metadata
[tool.poetry.dependencies] Main dependencies
[tool.poetry.group.dev.dependencies] Development dependencies
[build-system] Defines how the project is built

πŸ“ This is the Poetry-specific format. Modern Poetry versions also support other configuration options.

8. 🏭 Runtime vs development dependencies

Imagine your backend is deployed to a cloud host. Does production need pytest to serve an API request? No. Does it need FastAPI? Yes. So we split dependencies in two:

πŸš€ Runtime dependencies (needed when the app runs)

  • FastAPI, SQLAlchemy, Uvicorn, Pydantic

πŸ› οΈ Development dependencies (needed while developing, checking or testing)

  • pytest, mypy, Ruff, pytest-cov

Why separate them? Production should contain only what it needs to run, where practical. 🧹

9. πŸ”’ What is semantic versioning?

Versions look like 2.4.1, which means MAJOR.MINOR.PATCH:

Part Meaning
2 Major: may introduce breaking changes
4 Minor: adds backward-compatible features
1 Patch: fixes bugs
2.4.1 β†’ 2.4.2   patch update 🩹
2.4.1 β†’ 2.5.0   minor update ✨
2.4.1 β†’ 3.0.0   major update πŸ’₯
Enter fullscreen mode Exit fullscreen mode

🎯 What do version constraints mean?

fastapi = "^0.115"
Enter fullscreen mode Exit fullscreen mode

The caret ^ allows compatible versions according to Poetry's rules:

  • ^0.115 allows 0.115.0 up to, but not including, 0.116.0
  • ^2.0 allows 2.0.0 up to, but not including, 3.0.0

🏷️ PART 3: Type Hints in Python

10. πŸ€” What are type hints?

def add(a, b):
    return a + b
Enter fullscreen mode Exit fullscreen mode

What types should a and b be? Integers? Strings? Lists? Python doesn't say:

print(add(10, 20))            # 30
print(add("Hello ", "Deepika"))  # Hello Deepika
Enter fullscreen mode Exit fullscreen mode

Both work! Python is dynamically typed: it figures out types at runtime. But in a big backend we want our code to communicate what it expects.

πŸ“Œ Type hints are annotations that tell developers and static analysis tools what type a variable, parameter, or return value is expected to have.

Without type hints:

def greet(name):
    return "Hello " + name
Enter fullscreen mode Exit fullscreen mode

With type hints:

def greet(name: str) -> str:
    return "Hello " + name
Enter fullscreen mode Exit fullscreen mode
Syntax Meaning
name Parameter name
: str Expected parameter type is string
-> str Expected return type is string

🚨 Python does NOT enforce type hints at runtime!

A very common beginner misconception:

def greet(name: str) -> str:
    return "Hello " + name


print(greet(100))
Enter fullscreen mode Exit fullscreen mode

Python still lets you call it, but raises a TypeError when it tries to join a string and an integer. Type hints are not runtime validation. They help tools and developers detect mistakes.

11. πŸ“š Common types used in type hints

Python type Represents Example
int Integer 25
float Decimal number 9.5
str Text "Deepika"
bool True or false True
list Collection of values [1, 2, 3]
dict Key-value pairs {"name": "Deepika"}
tuple Ordered fixed collection (10, 20)
set Unique values {1, 2, 3}
def calculate_age(age: int) -> int:
    return age + 1


def get_username(name: str) -> str:
    return name.upper()


def is_adult(age: int) -> bool:
    return age >= 18
Enter fullscreen mode Exit fullscreen mode

πŸ•³οΈ A function that returns nothing

def display_message(message: str) -> None:
    print(message)
Enter fullscreen mode Exit fullscreen mode

None means the function doesn't return a meaningful value. It performs an action.

result = display_message("Hello")
print(result)
Enter fullscreen mode Exit fullscreen mode
Hello
None
Enter fullscreen mode Exit fullscreen mode

12. 🧺 Type hints for collections

def display_users(users: list[str]) -> None:
    for user in users:
        print(user)


display_users(["Deepika", "Jeeva", "Rahul"])
Enter fullscreen mode Exit fullscreen mode
  • list β†’ it's a list
  • str β†’ every element is expected to be a string
def calculate_total(numbers: list[int]) -> int:
    return sum(numbers)
Enter fullscreen mode Exit fullscreen mode

πŸ“– Dictionaries

def get_user() -> dict[str, str]:
    return {
        "name": "Deepika",
        "username": "deepika123",
    }
Enter fullscreen mode Exit fullscreen mode

dict[str, str] means keys are strings and values are strings. For mixed values:

def get_profile() -> dict[str, int | str]:
    return {
        "username": "deepika123",
        "age": 21,
    }
Enter fullscreen mode Exit fullscreen mode

The | symbol means either type.

13. πŸ”€ Union types

def get_user_id(user_id: int | str) -> str:
    return str(user_id)


get_user_id(101)
get_user_id("USER101")
Enter fullscreen mode Exit fullscreen mode

Both are valid. Before Python 3.10 you'd see:

from typing import Union


def get_user_id(user_id: Union[int, str]) -> str:
    return str(user_id)
Enter fullscreen mode Exit fullscreen mode

Same idea, older syntax.

14. ❓ Optional types

Very useful in backend work. A user may or may not have a profile picture:

def get_profile_picture() -> str | None:
    return "https://example.com/profile.jpg"
Enter fullscreen mode Exit fullscreen mode

It can return a string URL, or None if there's no picture.

image_url = get_profile_picture()

if image_url is not None:
    print(image_url)
else:
    print("No profile picture")
Enter fullscreen mode Exit fullscreen mode

Before Python 3.10:

from typing import Optional


def get_profile_picture() -> Optional[str]:
    return None
Enter fullscreen mode Exit fullscreen mode

⚠️ Optional[str] means str | None. Optional does not mean the parameter can be omitted. It means the value can be None.

15. ⚑ Type hints in your FastAPI project

Without type hints:

def get_post(post_id):
    post = repository.find(post_id)
    return post
Enter fullscreen mode Exit fullscreen mode

With type hints:

def get_post(post_id: int) -> dict | None:
    post = repository.find(post_id)
    return post
Enter fullscreen mode Exit fullscreen mode

Now anyone reading knows post_id is an integer and the function returns a dictionary or None. In real apps you'd use SQLAlchemy models or Pydantic schemas instead of a generic dict:

def get_post(post_id: int) -> Post | None:
    return repository.get_by_id(post_id)
Enter fullscreen mode Exit fullscreen mode

16. πŸ”Ž What is static type checking?

def calculate_total(price: int, quantity: int) -> int:
    return price * quantity


total = calculate_total("100", 5)  # 😬 string instead of int
Enter fullscreen mode Exit fullscreen mode

Python may not complain until execution reaches an operation that can't handle those values. Can we catch it before running? Yes, with static type checking: analyzing your source code without executing it. Think of a teacher checking your answer sheet before you submit. πŸ“

17. 🧐 What is mypy?

mypy is a static type checker for Python. It checks whether your code agrees with its type annotations.

pip install mypy
Enter fullscreen mode Exit fullscreen mode

Create example.py:

def add(a: int, b: int) -> int:
    return a + b


result = add("10", 20)
Enter fullscreen mode Exit fullscreen mode
mypy example.py
Enter fullscreen mode Exit fullscreen mode

mypy reports an error because "10" is a string but the function expects an integer. ❌ Fix it with add(10, 20), run mypy again, and it passes. βœ…

What mypy helps detect:

  • Passing a string where an int is expected
  • Returning the wrong type from a function
  • Accessing attributes a type doesn't have
  • Incorrect function argument types
  • Using None where a non-optional value is expected

πŸƒ Does mypy execute your code?

No!

Python execution mypy
Executes your program Analyzes your source code
Finds runtime errors when they occur Finds potential type errors before execution
Performs actual operations Does not run your application

βš™οΈ Configure mypy in pyproject.toml

[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
Enter fullscreen mode Exit fullscreen mode
mypy app/
Enter fullscreen mode Exit fullscreen mode

⚠️ strict = true enables a collection of stricter checks. It may reveal many issues in an existing project, so you can adopt stricter checking gradually.


πŸ§ͺ PART 4: Testing with pytest

18. πŸ€” Why do we need testing?

Your social media backend has: registration, login, create post, like post, comment, follow, and fetch home feed. You test everything manually and it works. Then your mentor asks you to change the like feature. You change it, but accidentally comments stop working. You might not notice until someone tests manually. 😱

πŸ“Œ Testing means checking whether your code produces the expected result under specific conditions. The goal is to catch bugs early and stop previously working features from breaking.

19. πŸ€– What is automated testing?

Instead of re-checking everything by hand, we write code that checks our code:

def add(a, b):
    return a + b
Enter fullscreen mode Exit fullscreen mode

We can check that add(10, 20) == 30. If correct, the test passes βœ…. If not, it fails ❌.

20. πŸ† What is pytest?

pytest is a popular Python testing framework that helps you write and run automated tests.

pip install pytest
Enter fullscreen mode Exit fullscreen mode

A simple project:

myproject/
β”‚
β”œβ”€β”€ calculator.py
└── test_calculator.py
Enter fullscreen mode Exit fullscreen mode

calculator.py:

def add(a: int, b: int) -> int:
    return a + b


def subtract(a: int, b: int) -> int:
    return a - b
Enter fullscreen mode Exit fullscreen mode

test_calculator.py:

from calculator import add, subtract


def test_add():
    assert add(10, 20) == 30


def test_subtract():
    assert subtract(20, 10) == 10
Enter fullscreen mode Exit fullscreen mode

☝️ What is assert?

assert checks whether a condition is true.

assert 10 + 20 == 30   # true, nothing happens
assert 10 + 20 == 50   # false, raises AssertionError
Enter fullscreen mode Exit fullscreen mode

In pytest, assertions verify expected results.

▢️ How to run tests

From your project root:

pytest
Enter fullscreen mode Exit fullscreen mode
================ test session starts ================

test_calculator.py ..                         [100%]

================ 2 passed ===========================
Enter fullscreen mode Exit fullscreen mode

Two dots = two tests passed. πŸŽ‰ If a test fails, pytest shows the failure and explains the mismatch.

πŸ” Why do test files start with test_?

pytest automatically discovers files matching test_*.py or *_test.py, and functions starting with test_:

def test_login():
    ...
Enter fullscreen mode Exit fullscreen mode

No need to register tests manually. ✨

21. πŸͺœ Types of testing

Type What it tests Example
πŸ”¬ Unit One small unit (a function or service method) Does password validation work?
πŸ”— Integration Multiple components working together Does the API talk correctly to PostgreSQL?
🌍 End-to-End (E2E) A complete user workflow Register β†’ log in β†’ create post β†’ see it in feed

A unit test usually isolates the function being tested. An integration test deliberately connects multiple real components. pytest can be used for all of these.


🧰 PART 5: pytest Fixtures

22. πŸ€” What is a fixture?

Imagine 10 tests that each need a user object. Without fixtures:

def test_user_name():
    user = {"id": 1, "name": "Deepika"}
    assert user["name"] == "Deepika"


def test_user_id():
    user = {"id": 1, "name": "Deepika"}
    assert user["id"] == 1
Enter fullscreen mode Exit fullscreen mode

We're creating the same test data again and again. 😩

πŸ“Œ A fixture is a function that prepares and provides reusable data or resources to test functions. Think of it as a person who sets everything up before an experiment begins. πŸ§‘β€πŸ”¬

Step 1: create a fixture

import pytest


@pytest.fixture
def sample_user():
    return {
        "id": 1,
        "name": "Deepika",
        "username": "deepika123",
    }
Enter fullscreen mode Exit fullscreen mode

@pytest.fixture tells pytest: "This function provides a fixture other tests can request."

Step 2: use it

def test_user_name(sample_user):
    assert sample_user["name"] == "Deepika"


def test_user_id(sample_user):
    assert sample_user["id"] == 1
Enter fullscreen mode Exit fullscreen mode

We never called sample_user(). How does it work? This is pytest's fixture injection: when a test parameter's name matches a fixture, pytest automatically runs the fixture and passes its returned value into the test.

@pytest.fixture
sample_user()  β†’  pytest executes the fixture
        β”‚
        β–Ό
test_user_name(sample_user)  β†’  receives the returned dictionary
Enter fullscreen mode Exit fullscreen mode

23. 🎯 Fixture scopes

Should pytest recreate the same resource before every test? Sometimes yes, sometimes no.

Scope When the fixture is created
function Once for each test function (default)
class Once for each test class
module Once for each Python test module
package Once for a package
session Once for the entire test session
@pytest.fixture(scope="function")
def sample_user():
    return {"name": "Deepika"}


@pytest.fixture(scope="session")
def app_config():
    return {"environment": "test"}
Enter fullscreen mode Exit fullscreen mode

⚠️ A wider scope can improve performance, but it can cause tests to share mutable state. Use it carefully.

24. 🧹 yield in fixtures (setup + cleanup)

Testing database code, we need to: open a connection β†’ run the test β†’ close the connection. Use yield:

@pytest.fixture
def database_connection():
    print("Opening database")
    connection = "Database Connection"
    yield connection
    print("Closing database")
Enter fullscreen mode Exit fullscreen mode
  • Before yield β†’ setup
  • At yield β†’ provide the resource to the test
  • After yield β†’ cleanup
def test_database(database_connection):
    assert database_connection == "Database Connection"
Enter fullscreen mode Exit fullscreen mode

The cleanup code runs after the test finishes, even if the test fails. For real databases, you'd return an actual connection/session and close or roll it back in the teardown section.


πŸ” PART 6: pytest Parametrize

25. πŸ€” What is parametrize?

def is_eligible(age: int) -> bool:
    return age >= 18
Enter fullscreen mode Exit fullscreen mode

Test ages 15 β†’ False, 17 β†’ False, 18 β†’ True, 21 β†’ True, 25 β†’ True. Five separate test functions would be repetitive. parametrize runs the same test with different inputs:

import pytest


@pytest.mark.parametrize(
    "age, expected",
    [
        (15, False),
        (17, False),
        (18, True),
        (21, True),
        (25, True),
    ],
)
def test_voting_eligibility(age, expected):
    assert is_eligible(age) == expected
Enter fullscreen mode Exit fullscreen mode
  • @pytest.mark.parametrize( β†’ run this test with multiple sets of inputs
  • "age, expected" β†’ the parameter names
  • the list of tuples β†’ the test cases, where each tuple = one test execution
Test 1: age = 15 β†’ expected False
Test 2: age = 17 β†’ expected False
Test 3: age = 18 β†’ expected True
Test 4: age = 21 β†’ expected True
Test 5: age = 25 β†’ expected True

5 input combinations β†’ 5 test executions βœ…
Enter fullscreen mode Exit fullscreen mode

πŸ“ Useful for your backend

Test a valid username, an empty username, a username with spaces, a very long username, and a duplicate username, all with one parametrized test.

🧠 Fixture β†’ provides reusable test data/resources. Parametrize β†’ runs the same test with different inputs.


πŸ’ PART 7: Monkeypatch in pytest

26. πŸ€” The problem monkeypatch solves

Your backend sends an email after registration:

def send_email(email: str) -> str:
    print(f"Sending email to {email}")
    return "Email sent"
Enter fullscreen mode Exit fullscreen mode

When testing registration, you don't want to send a real email every time because:

  • πŸ“§ It may send actual emails
  • 🌐 It may need internet access
  • πŸ”‘ It may need credentials
  • 🐌 It makes tests slower
  • πŸ’Έ It may cost money

So we temporarily replace the real function with a fake one.

πŸ“Œ monkeypatch is a pytest feature that temporarily changes an object's attributes, functions, environment variables, or other values during a test. After the test, pytest automatically restores the original state.

27. πŸ› οΈ Simple monkeypatch example

email_service.py:

def send_email(email: str) -> str:
    return f"Real email sent to {email}"
Enter fullscreen mode Exit fullscreen mode

registration.py:

from email_service import send_email


def register_user(email: str) -> str:
    result = send_email(email)
    return result
Enter fullscreen mode Exit fullscreen mode

test_registration.py:

import registration


def test_register_user(monkeypatch):
    def fake_send_email(email: str) -> str:
        return "Fake email sent"

    monkeypatch.setattr(
        "registration.send_email",
        fake_send_email,
    )

    result = registration.register_user("test@example.com")

    assert result == "Fake email sent"
Enter fullscreen mode Exit fullscreen mode

The key line:

monkeypatch.setattr("registration.send_email", fake_send_email)
Enter fullscreen mode Exit fullscreen mode

It says: "Temporarily replace the send_email used by the registration module with my fake function." No real email is sent. πŸŽ‰

28. 🧰 Important monkeypatch methods

A. Set an attribute

monkeypatch.setattr(object, "attribute", new_value)
Enter fullscreen mode Exit fullscreen mode

B. Set an environment variable

monkeypatch.setenv("APP_ENV", "test")
Enter fullscreen mode Exit fullscreen mode

C. Delete an environment variable

monkeypatch.delenv("DATABASE_URL", raising=False)
Enter fullscreen mode Exit fullscreen mode

D. Change a dictionary value

config = {"debug": False}
monkeypatch.setitem(config, "debug", True)
Enter fullscreen mode Exit fullscreen mode

E. Change the working directory

monkeypatch.chdir("/tmp")
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Don't memorize every method. Understand the core idea: temporarily change something so you can control a test.


🎭 PART 8: Mocking with unittest.mock

29. πŸ€” What is mocking?

Your registration workflow:

User Registration
       β”‚
       β–Ό
Validate User
       β”‚
       β–Ό
Save User in Database
       β”‚
       β–Ό
Send Welcome Email
       β”‚
       β–Ό
Return Success
Enter fullscreen mode Exit fullscreen mode

To test registration, you don't want to connect to a real database, send a real email, call a payment API, or contact a third-party service. You want to isolate your registration logic.

πŸ“Œ Mocking replaces real dependencies with controlled test doubles. These fake objects let you control responses and check how your code interacts with them.

30. πŸ“¦ What is unittest.mock?

A built-in Python library (no install needed) with tools like Mock, MagicMock, patch, call_args, and assert_called_once_with().

31. 🎭 What is Mock?

A Mock is a fake object that can imitate another object.

from unittest.mock import Mock

email_sender = Mock()
email_sender.send.return_value = "Email sent"

result = email_sender.send("test@example.com")
print(result)
Enter fullscreen mode Exit fullscreen mode
Email sent
Enter fullscreen mode Exit fullscreen mode
  • Mock() creates a fake object
  • .send.return_value = "Email sent" tells the fake send() what to return
  • email_sender.send(...) calls the fake method. No real email is sent.

32. πŸ”™ return_value

from unittest.mock import Mock

database = Mock()
database.get_user.return_value = {"id": 1, "name": "Deepika"}

user = database.get_user(1)
print(user)
Enter fullscreen mode Exit fullscreen mode
{'id': 1, 'name': 'Deepika'}
Enter fullscreen mode Exit fullscreen mode

You simulate database responses without a database. πŸ—„οΈ

33. πŸ’₯ side_effect

What if your database connection fails? Use side_effect to raise an exception:

from unittest.mock import Mock

database = Mock()
database.get_user.side_effect = Exception("Database error")

database.get_user(1)  # raises Exception: Database error
Enter fullscreen mode Exit fullscreen mode

It can also return different values on successive calls:

service = Mock()
service.get_data.side_effect = [10, 20, 30]

print(service.get_data())  # 10
print(service.get_data())  # 20
print(service.get_data())  # 30
Enter fullscreen mode Exit fullscreen mode
Feature Purpose
return_value Define what a mock returns
side_effect Define behavior such as exceptions or different successive results

34. 🩹 What is patch?

Probably the most important part of unittest.mock. patch temporarily replaces an existing object with a mock.

payment_service.py:

def process_payment(amount: int) -> str:
    return "Real payment processed"
Enter fullscreen mode Exit fullscreen mode

order.py:

from payment_service import process_payment


def place_order(amount: int) -> str:
    result = process_payment(amount)
    return result
Enter fullscreen mode Exit fullscreen mode

test_order.py:

from unittest.mock import patch

import order


@patch("order.process_payment")
def test_place_order(mock_payment):
    mock_payment.return_value = "Payment successful"

    result = order.place_order(500)

    assert result == "Payment successful"
Enter fullscreen mode Exit fullscreen mode
  • @patch("order.process_payment") temporarily replaces process_payment in the order module
  • mock_payment.return_value = ... configures what the mock returns
  • order.place_order(500) runs the real order function, but its payment dependency is fake

🚨 Extremely important: patch where the object is used!

Why @patch("order.process_payment") and not @patch("payment_service.process_payment")?

Because order.py did this:

from payment_service import process_payment
Enter fullscreen mode Exit fullscreen mode

That created a name process_payment inside the order module, and place_order() uses that name. So we patch the name where the code under test looks it up. This is one of the most common mocking mistakes! ⚠️

35. βœ… assert_called_once_with

Verify your app actually called the payment function with the right amount:

mock_payment.assert_called_once_with(500)
Enter fullscreen mode Exit fullscreen mode

This checks that the mock was called exactly once and with argument 500.

Method What it checks
assert_called() Called at least once
assert_not_called() Never called
assert_called_once() Called exactly once
assert_called_with(500) Most recent call used argument 500
assert_called_once_with(500) Exactly one call, with argument 500
call_count Number of calls

36. ✨ What is MagicMock?

MagicMock is a more feature-rich Mock that also supports Python's special (magic/dunder) methods:

from unittest.mock import MagicMock

items = MagicMock()
items.__len__.return_value = 5

print(len(items))  # 5
Enter fullscreen mode Exit fullscreen mode

Other special methods: __iter__(), __getitem__(), __enter__(), __exit__(). You'll often see MagicMock with context managers, iterables, and container-like objects. For ordinary function mocking, Mock is usually enough.


βš”οΈ PART 9: Monkeypatch vs Mock vs Patch

🎯 Very important for interviews and real backend work!

Concept Meaning
Mocking General technique of replacing real dependencies with test doubles
Mock Creates a fake object
MagicMock A Mock that also supports magic methods
patch Temporarily replaces an object with a mock
monkeypatch pytest fixture for temporarily changing attributes, environment, dicts, etc.

Use monkeypatch when you want to:

  • πŸ”„ Replace a function
  • 🌍 Modify environment variables
  • βš™οΈ Change a configuration value
  • ♻️ Have pytest restore the original value automatically

Use unittest.mock when you want to:

  • πŸ”Œ Simulate an external dependency
  • πŸ”™ Define return values
  • πŸ’₯ Simulate exceptions
  • πŸ”’ Verify how many times a function was called
  • πŸ” Inspect the arguments passed to a dependency

Both can overlap. You don't need to use them together in every test.


πŸ—οΈ PART 10: Putting It All Together in a Backend

37. πŸ§ͺ How to test a real backend service

API Router
    β”‚
    β–Ό
User Service
    β”‚
    β–Ό
User Repository
    β”‚
    β–Ό
PostgreSQL Database
Enter fullscreen mode Exit fullscreen mode

To test UserService, you don't need PostgreSQL every time. Mock the repository.

user_service.py:

class UserService:
    def __init__(self, repository):
        self.repository = repository

    def get_user(self, user_id: int):
        user = self.repository.get_by_id(user_id)

        if user is None:
            raise ValueError("User not found")

        return user
Enter fullscreen mode Exit fullscreen mode

test_user_service.py:

from unittest.mock import Mock

import pytest

from user_service import UserService


def test_get_user():
    repository = Mock()
    repository.get_by_id.return_value = {"id": 1, "name": "Deepika"}

    service = UserService(repository)

    result = service.get_user(1)

    assert result["name"] == "Deepika"
    repository.get_by_id.assert_called_once_with(1)
Enter fullscreen mode Exit fullscreen mode

What happens:

  1. Create a fake repository
  2. Tell it what data to return
  3. Create the real UserService
  4. Call get_user()
  5. The service calls the mocked repository
  6. The mock returns the predefined user
  7. Assert the correct user was returned
  8. Verify the repository was called with ID 1

We're testing the actual service logic without a real database. That's a unit test. βœ…

❌ Testing the error case

def test_get_user_not_found():
    repository = Mock()
    repository.get_by_id.return_value = None

    service = UserService(repository)

    with pytest.raises(ValueError, match="User not found"):
        service.get_user(100)
Enter fullscreen mode Exit fullscreen mode

pytest.raises(ValueError) means we expect the code inside to raise a ValueError. If it doesn't, the test fails. This is exception testing.

38. πŸ—‚οΈ What is conftest.py?

With 100+ tests, many files need the same fixtures. Instead of redefining them everywhere, pytest has a special file: conftest.py.

backend/
β”‚
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ main.py
β”‚   β”œβ”€β”€ services/
β”‚   └── repositories/
β”‚
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ conftest.py
β”‚   β”œβ”€β”€ test_users.py
β”‚   β”œβ”€β”€ test_posts.py
β”‚   └── test_auth.py
β”‚
└── pyproject.toml
Enter fullscreen mode Exit fullscreen mode

conftest.py:

import pytest


@pytest.fixture
def sample_user():
    return {"id": 1, "name": "Deepika"}
Enter fullscreen mode Exit fullscreen mode

Tests in that directory and its subdirectories can use the fixture without importing it:

def test_user_name(sample_user):
    assert sample_user["name"] == "Deepika"
Enter fullscreen mode Exit fullscreen mode
  • test_*.py β†’ contains tests
  • conftest.py β†’ shared pytest configuration and fixtures
  • @pytest.fixture β†’ reusable test setup

39. ⚑ TestClient in FastAPI

@app.get("/health")
def health_check():
    return {"status": "ok"}
Enter fullscreen mode Exit fullscreen mode

You could start the real server and send requests manually, but there's a simpler way: Starlette's TestClient.

from fastapi.testclient import TestClient

from app.main import app

client = TestClient(app)


def test_health_endpoint():
    response = client.get("/health")

    assert response.status_code == 200
    assert response.json() == {"status": "ok"}
Enter fullscreen mode Exit fullscreen mode
  1. TestClient(app) creates a test client for your FastAPI app
  2. client.get("/health") sends a simulated HTTP request through the app
  3. response.status_code gives the HTTP status code
  4. response.json() gives the response body as a Python dict

No need to start Uvicorn manually. This is API-level testing. 🎯

πŸ“ If your app uses async resources or async test functions, you may also meet httpx.AsyncClient and pytest-asyncio.


🧩 PART 11: How Everything Fits Together

Your Python backend toolkit:

Tool Role
🐍 Python The programming language
🏠 venv / Poetry Environment and dependency management
πŸ“„ pyproject.toml Project metadata and tool configuration
🏷️ Type hints + mypy Type documentation and static checking
πŸ§ͺ pytest Automated testing framework
🎭 Mocking & monkeypatch Isolating dependencies during testing

40. πŸ“‹ Commands cheat sheet

Command Purpose
python -m venv .venv Create virtual environment
.venv\Scripts\Activate.ps1 Activate on Windows PowerShell
source .venv/bin/activate Activate on Linux/macOS
deactivate Deactivate environment
pip install fastapi Install a package
pip list List installed packages
pip freeze Display installed versions
pip freeze > requirements.txt Save dependencies
pip install -r requirements.txt Install from file
poetry init Initialize Poetry project
poetry add fastapi Add dependency
poetry install Install project dependencies
poetry run pytest Run tests using Poetry
mypy app/ Check application types
pytest Run tests
pytest -v Run tests with detailed names
pytest -q Run tests in concise mode
pytest -x Stop after first failure
pytest -k login Run tests matching a keyword
pytest --collect-only Show discovered tests

🎀 PART 12: Common Interview Questions

1. What is a virtual environment?
An isolated Python environment that keeps project-specific dependencies without affecting other projects.

2. What's the difference between venv and Poetry?
venv creates isolated environments. Poetry provides broader dependency, environment and packaging management.

3. What is pyproject.toml?
A modern Python project configuration file that stores project metadata and tool configurations.

4. What is a lock file?
A file that records exact resolved dependency versions to help reproduce a consistent environment.

5. What are type hints?
Annotations that specify expected types for variables, parameters and return values.

6. Does Python enforce type hints automatically?
No. Python generally doesn't enforce them at runtime. Static analysis tools such as mypy can check them.

7. What is mypy?
A static type checker that analyzes Python source code and reports potential type inconsistencies.

8. What is pytest?
A Python testing framework used to write and run automated tests.

9. What is a fixture?
A reusable function that provides test data or prepares resources required by tests.

10. What is parametrize?
A pytest feature that runs the same test with multiple input combinations.

11. What is monkeypatch?
A pytest fixture that temporarily modifies attributes, functions, environment variables or other values during a test.

12. What is mocking?
Replacing real dependencies with controlled fake objects to isolate the code being tested.

13. What's the difference between Mock and MagicMock?
MagicMock supports Python's magic methods in addition to everything Mock can do.

14. What's the difference between return_value and side_effect?
return_value specifies a mock's returned value. side_effect can raise exceptions or give different results on successive calls.

15. What's the difference between unit and integration testing?
Unit testing checks an isolated component. Integration testing checks how multiple components work together.


πŸ§ͺ PART 13: Test Your Understanding!

Try answering before revealing the answers. No stress! ❀️

  1. Two Python projects need different versions of FastAPI. What should you use?

    • A) A single global installation
    • B) Separate virtual environments
    • C) Only type hints
  2. Which file records exact resolved dependency versions in a Poetry project?

    • A) main.py
    • B) conftest.py
    • C) poetry.lock
  3. What does name: str mean in a function?

    • A) name is always checked at runtime
    • B) name is expected to be a string
    • C) name is converted into a string automatically
  4. Which tool checks type annotations without executing your application?

    • A) mypy
    • B) pytest
    • C) Uvicorn
  5. You want to reuse a sample user object across multiple tests. What do you use?

    • A) parametrize
    • B) A fixture
    • C) side_effect
  6. You want to run the same test with 10 different inputs. Which feature helps?

    • A) monkeypatch
    • B) MagicMock
    • C) parametrize
  7. You want to temporarily replace an environment variable during a test. What's suitable?

    • A) monkeypatch.setenv()
    • B) pytest.raises()
    • C) mypy
  8. You want a fake database method to return a predefined user. What do you use?

    • A) Mock.return_value
    • B) venv
    • C) poetry.lock
  9. You want to check that a mocked email function was called exactly once with a particular address. Which method?

    • A) assert_called()
    • B) assert_called_once_with()
    • C) assert_not_called()
  10. What does the code after yield in a fixture usually do?

    • A) Dependency installation
    • B) Static checking
    • C) Cleanup

πŸ‘€ Click to reveal answers
  1. B: Separate virtual environments isolate each project's packages
  2. C: poetry.lock records exact resolved versions
  3. B: name is expected to be a string (not enforced at runtime)
  4. A: mypy analyzes code without running it
  5. B: A fixture provides reusable test data
  6. C: parametrize runs one test with many inputs
  7. A: monkeypatch.setenv() sets an env variable temporarily
  8. A: Mock.return_value defines what the fake returns
  9. B: assert_called_once_with() checks both call count and arguments
  10. C: Cleanup (teardown) runs after the test finishes


πŸ‹οΈ PART 14: What You Should Actually Practise

For your backend project, follow this order:

  1. 🏠 Create a .venv and install FastAPI and pytest
  2. πŸ“ Create a requirements.txt, or understand your project's existing Poetry setup
  3. 🏷️ Add type hints to a few service and repository functions
  4. 🧐 Run mypy and understand its errors
  5. πŸ§ͺ Write unit tests for your user service
  6. πŸ—‚οΈ Create shared fixtures in conftest.py
  7. πŸ” Use parametrize to test different inputs
  8. πŸ’ Use monkeypatch to replace environment variables or functions
  9. 🎭 Use Mock and patch to simulate repository and external service behavior
  10. ⚑ Write a FastAPI endpoint test using TestClient

πŸ’‘ You don't need every tool in every project. If your repository already uses a particular dependency manager, follow its existing setup instead of introducing Poetry or another tool unnecessarily.

🏁 Final Mental Model: Remember These Ten Things

Topic Remember it as
🏠 venv A separate room for each project's libraries
🎭 Poetry Project dependency and packaging manager
πŸ“„ pyproject.toml Project configuration file
🏷️ Type hints Expected types written in code
🧐 mypy Checks type consistency without running the app
πŸ§ͺ pytest Automatically tests your code
🧰 Fixtures Prepare reusable test resources
πŸ” Parametrize Same test, different inputs
πŸ’ Monkeypatch Temporarily change something
🎭 Mocking Replace real dependencies with fake ones

πŸš€ What's Next?

Don't just memorize definitions. Write one small test for a real function from your own backend. That's when everything clicks! πŸ’‘

If this post helped you, drop a ❀️ or πŸ¦„ and share it with a friend learning Python. Happy coding! πŸ‘©β€πŸ’»βœ¨

Top comments (0)