**Why testing matters and why pytest**
Tests are executable documentation. They prove your code does what you think it does, and they catch regressions when you change something later. pytest is the de facto standard: less boilerplate than `unittest`, powerful parametrize, and an enormous plugin ecosystem.
**Writing your first pytest test**
Any function starting with `test_` in a file starting with `test_` is discovered automatically:
```python
# test_math.py
def add(a, b):
return a + b
def test_add_integers():
assert add(2, 3) == 5
def test_add_floats():
assert add(0.1, 0.2) == pytest.approx(0.3) # float comparison
```
Run with: `pytest test_math.py` or just `pytest` to discover all tests.
**pytest assertions vs unittest**
pytest rewrites `assert` statements to show detailed diffs on failure. No special assert methods needed:
```python
# unittest style:
self.assertEqual(result, [1, 2, 3])
self.assertIn('key', mapping)
self.assertRaises(ValueError, func, bad_arg)
# pytest style — plain Python:
assert result == [1, 2, 3]
assert 'key' in mapping
with pytest.raises(ValueError):
func(bad_arg)
```
**`@pytest.mark.parametrize` — testing multiple inputs**
Instead of copy-pasting tests for each case, parametrize runs one test function with many different inputs:
```python
import pytest
@pytest.mark.parametrize('a, b, expected', [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
(100, -50, 50),
])
def test_add(a, b, expected):
assert add(a, b) == expected
# pytest reports each case separately:
# test_add[1-2-3] PASSED
# test_add[0-0-0] PASSED
# test_add[-1-1-0] PASSED
# test_add[100--50-50] PASSED
```
**Testing exceptions with context**
```python
def divide(a, b):
if b == 0:
raise ZeroDivisionError('cannot divide by zero')
return a / b
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError, match='cannot divide'):
divide(10, 0)
def test_divide_by_zero_type():
with pytest.raises(ZeroDivisionError) as exc_info:
divide(10, 0)
assert 'zero' in str(exc_info.value)
```
Fixtures, scope, conftest.py, mock, and monkeypatch
**Fixtures — reusable test setup**
A fixture is a function decorated with `@pytest.fixture` that sets up shared state for tests. pytest injects fixtures by parameter name:
```python
import pytest
@pytest.fixture
def user():
return {'name': 'Alice', 'role': 'admin'}
def test_admin_access(user): # pytest injects 'user' automatically
assert user['role'] == 'admin'
def test_user_name(user):
assert user['name'] == 'Alice'
```
**Fixture teardown with `yield`**
```python
@pytest.fixture
def db_connection():
conn = connect_to_test_db()
yield conn # setup returns here
conn.close() # teardown runs after test completes
conn.delete_all_rows() # cleanup
```
**Fixture scope**
By default fixtures are created fresh for each test (`scope='function'`). You can share one instance across multiple tests to avoid expensive setup:
```python
@pytest.fixture(scope='module') # one instance per test module
def expensive_resource():
return setup_slow_external_service()
@pytest.fixture(scope='session') # one instance for the whole test run
def db_schema():
return create_schema()
```
**`conftest.py` — shared fixtures across files**
Put fixtures in `conftest.py` and they're automatically available to all tests in the same directory and subdirectories — no import needed:
```
tests/
conftest.py <- fixtures defined here
test_users.py <- uses fixtures from conftest
test_orders.py <- also uses fixtures from conftest
api/
conftest.py <- additional fixtures for just this subdir
test_routes.py
```
**`unittest.mock` — replacing dependencies**
Mock lets you replace parts of your system with fake objects during tests:
```python
from unittest.mock import Mock, patch, MagicMock
# patch: temporarily replaces an attribute/function
@patch('mymodule.requests.get')
def test_fetch_data(mock_get):
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {'key': 'value'}
result = fetch_data('https://api.example.com')
assert result == {'key': 'value'}
mock_get.assert_called_once_with('https://api.example.com')
```
**`monkeypatch` — pytest's built-in patcher**
For simple cases, pytest's `monkeypatch` fixture is cleaner than `@patch`:
```python
def test_home_dir(monkeypatch, tmp_path):
monkeypatch.setenv('HOME', str(tmp_path))
monkeypatch.setattr(os.path, 'exists', lambda p: True)
# changes are automatically reverted after the test
assert get_config_dir() == str(tmp_path / '.config')
```
Test isolation, factory fixtures, marks, coverage, CLI reference
**Test isolation principles**
Each test should be fully independent — it should set up its own state, run, and clean up. Tests that share mutable state cause flaky results (tests pass in isolation but fail when run together).
```python
# BAD: tests share module-level state
items = []
def test_add_item():
items.append('a')
assert len(items) == 1
def test_count(): # FAILS when run after test_add_item
assert len(items) == 0
# GOOD: fixture creates fresh state per test
@pytest.fixture
def items():
return [] # new list for each test
```
**Factory fixtures**
A fixture can return a function that creates objects on demand — useful when tests need different variations of the same thing:
```python
@pytest.fixture
def make_user():
def _make(name='Alice', role='user', active=True):
return User(name=name, role=role, active=active)
return _make
def test_admin(make_user):
admin = make_user(role='admin')
assert admin.can_delete()
def test_inactive(make_user):
user = make_user(active=False)
assert not user.can_login()
```
**pytest marks — skipping and marking tests**
```python
import pytest
@pytest.mark.skip(reason='not implemented yet')
def test_future_feature():
...
@pytest.mark.skipif(sys.platform == 'win32', reason='Unix-only')
def test_unix_socket():
...
@pytest.mark.xfail(reason='known bug in upstream')
def test_known_broken():
... # expected to fail; XPASS (unexpected pass) also reported
# Custom marks for grouping:
@pytest.mark.slow
def test_full_integration():
...
# Run only slow tests: pytest -m slow
# Skip slow tests: pytest -m 'not slow'
```
**Coverage — measuring what's tested**
```bash
pip install pytest-cov
pytest --cov=mymodule --cov-report=term-missing
```
Coverage shows which lines were never reached by any test. Aim for high coverage on core logic, but don't chase 100% — some code paths (startup, error handlers) are legitimately hard to test.
**Quick reference — pytest CLI**
| Command | What it does |
|---|---|
| `pytest` | Run all tests |
| `pytest test_foo.py` | Run one file |
| `pytest test_foo.py::test_bar` | Run one test |
| `pytest -k 'add'` | Run tests matching 'add' |
| `pytest -v` | Verbose output |
| `pytest -x` | Stop at first failure |
| `pytest --lf` | Re-run last failed tests only |
| `pytest -s` | Don't capture stdout (show print()) |
Write three pytest test functions for a `add(a, b)` function: `test_add_positive`, `test_add_negative`, and `test_add_zero`. Each should call `add()` and use `assert` to check the result.
Write tests for a `divide(a, b)` function. Test that `divide(10, 2)` returns `5.0`, and that `divide(10, 0)` raises a `ZeroDivisionError` using `pytest.raises`.
import pytest
def divide(a: float, b: float) -> float:
return a / b
def test_divide_normal():
pass
def test_divide_by_zero():
pass
Solution
import pytest
def divide(a: float, b: float) -> float:
return a / b
def test_divide_normal():
assert divide(10, 2) == 5.0
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
divide(10, 0)
Write a `@pytest.fixture` called `sample_list` that returns `[3, 1, 4, 1, 5, 9]`. Write two tests that use it: `test_length` checks `len == 6`, and `test_max` checks `max == 9`.
Use `@pytest.mark.parametrize` to test `is_palindrome(s)` with five cases: `'racecar'` → True, `'hello'` → False, `'madam'` → True, `'a'` → True, `'ab'` → False. Write all five as a single parametrized test.
import pytest
def is_palindrome(s: str) -> bool:
return s == s[::-1]
@pytest.mark.parametrize('s, expected', [
# fill in the 5 cases
])
def test_is_palindrome(s, expected):
pass
Write a function `get_debug_mode() -> bool` that reads the `DEBUG` environment variable and returns `True` if it equals `'1'`, else `False`. Write two tests using `monkeypatch.setenv`: one where `DEBUG=1` → True, one where `DEBUG=0` → False.
A function `get_username()` calls `os.getlogin()` which may not work in CI. Use `monkeypatch.setattr` to replace `os.getlogin` with a lambda that returns `'testuser'`. Verify that `get_username()` returns `'testuser'`.
Write a fixture `temp_list` that creates a list `[1, 2, 3]`, yields it, then clears it after the test (teardown using `yield`). Write a test `test_append` that appends `4` and checks `len == 4`. After the test, the fixture's teardown should clear the list.
import pytest
@pytest.fixture
def temp_list():
data = [1, 2, 3]
yield data
# teardown: clear after test
pass
def test_append(temp_list):
temp_list.append(4)
assert len(temp_list) == 4
Solution
import pytest
@pytest.fixture
def temp_list():
data = [1, 2, 3]
yield data
data.clear()
def test_append(temp_list):
temp_list.append(4)
assert len(temp_list) == 4
Write a function `write_and_read(path, text)` that writes `text` to `path`, reads it back, and returns the content. Use the built-in `tmp_path` fixture to test it without touching the real filesystem.
from pathlib import Path
def write_and_read(path: Path, text: str) -> str:
path.write_text(text)
return path.read_text()
def test_write_and_read(tmp_path):
file = tmp_path / 'test.txt'
result = write_and_read(file, 'hello')
assert result == 'hello'
Solution
from pathlib import Path
def write_and_read(path: Path, text: str) -> str:
path.write_text(text)
return path.read_text()
def test_write_and_read(tmp_path):
file = tmp_path / 'test.txt'
result = write_and_read(file, 'hello')
assert result == 'hello'
Given a `Counter` class with `increment()`, `decrement()`, and `value` property, write a fixture `counter` that returns a fresh `Counter()`. Write tests: `test_initial_value` (value == 0), `test_increment` (value == 1 after one increment), `test_decrement` (value == -1 after one decrement).
Write tests for a `circle_area(r)` function (returns `math.pi * r ** 2`). Use `pytest.approx` to compare the result — plain `==` fails for floats. Test with `r=1` (expected ~3.14159), `r=2` (expected ~12.566), and `r=0` (expected 0).
We use necessary cookies to run the site. With your permission, we can also save your site preferences and use analytics and advertising cookies to understand usage and support the project.
Open tools in tabs.Exercises, IDE tools, and trainers stay available as site tabs.
Switch without losing context.Move between explanations, code, and utilities while keeping your place.
Use the sidebar as your map.The left panels hold navigation, settings, files, libraries, and tool controls.
PythonJavaScriptSQLite
One IDE, three practical modes
Python in the browser.Run small scripts, try libraries, and practice API requests without installing anything.
JavaScript for quick experiments.Test browser-friendly code and compare ideas next to your learning materials.
SQLite for data practice.Open the database explorer to inspect tables, write queries, and learn SQL workflows locally.
TopicIDE
Work side by side with split tabs
Keep instructions visible.Open an exercise or reference page beside the IDE instead of jumping back and forth.
Compare tools while you learn.Place regex checks, explanations, and code experiments next to each other when the task needs it.
Close the split when you are done.The workspace returns to a single focused tab, and your open site tabs remain available.
Code Typing Trainer
Or plain text
This trainer is designed for a physical keyboard.Open this section on a laptop or desktop with a wide screen. Touch typing practice will not work correctly on a phone.
Speed: 0 chars/min
0 words/min
Best speed (60s): 0 chars/min
0 words/min
Errors: 0
Total time: 0.0 s
To practice touch typing, avoid looking at your physical keyboard.