How to Write Easily Testable Python Code: A Step-by-Step Guide
Introduction
Writing unit tests is one of the most empowering skills you can develop as a programmer. It transforms your ability to build reliable, maintainable software. But tests are only as good as the code they test. The secret to effortless testing lies in making your code prime testable—a term for code that has no side effects and is deterministic. This guide will show you how to identify, isolate, and write such code, making testing a breeze. By the end, you'll have a repeatable process to refactor any codebase into a test-friendly structure.

What You Need
- A working Python environment (version 3.6+)
- A codebase you want to test (even a small script works)
- A testing framework (e.g.,
pytestorunittest) - Familiarity with basic Python functions and classes
- Willingness to refactor—sometimes heavily
Step-by-Step Process
Step 1: Identify Code That Is Hard to Test
Before you can improve testability, you need to spot the troublemakers. Look for functions or methods that:
- Have side effects: modify a database, write to a file, change a global variable, send an email, or open a garage door.
- Are non-deterministic: return different results for the same inputs—like fetching the current time, a random number, or an API response.
For example, a function that logs in a user, writes to a database, and returns a session token is both side-effect‑laden and non-deterministic. It's hard to test without mocking everything.
Step 2: Extract the Pure Core
Once you've identified a hard-to-test function, look for any logic that can be separated into a pure function—one with no side effects and deterministic behavior. This is the "prime testable" part. Ask yourself: What calculation or transformation is happening inside this function that doesn't depend on external state?
Suppose you have a function that validates a password against a stored hash. The hashing algorithm itself is deterministic: given the same password and salt, it always produces the same hash. Extract that hashing step into its own function. Similarly, string parsing, mathematical computations, and data transformations are often pure.
Step 3: Refactor to Isolate Prime‑Testable Components
Now refactor your code so that the pure part lives in its own function or method. The remaining code (which must have side effects or be non-deterministic) should call that pure function. This creates a clean separation. For example:
# Before: hard to test
def create_user(username, plain_password):
# side effect: write to DB
db.save(username, plain_password)
return True
# After: prime testable core extracted
def _hash_password(plain_password):
import hashlib
# deterministic, no side effects
return hashlib.sha256(plain_password.encode()).hexdigest()
def create_user(username, plain_password):
hashed = _hash_password(plain_password)
# side effect kept separate
db.save(username, hashed)
return True
Now _hash_password is prime testable. You can test it without mocking anything.

Step 4: Write Tests for the Prime‑Testable Functions
With the pure function isolated, writing tests becomes straightforward. Use your testing framework to call the function with known inputs and assert the expected outputs. Because there are no side effects, you don't need mocks or fixtures. For example:
def test_hash_password():
result = _hash_password("supersecret")
expected = "2f77668a9dfbf8d5848b9e3a..." # known hash
assert result == expected
Test edge cases like empty strings, special characters, and very long inputs. Since the function is deterministic, these tests are reliable and fast.
Step 5: Integrate Back and Test the Outer Function
Now that the core logic is tested, you can write integration tests for the outer function that still has side effects. Use mocks or stubs for the database calls. Because the pure part is already verified, your integration tests can focus on whether the side effect happens correctly (e.g., the right data is saved).
Step 6: Repeat for Every Complex Module
Go through your codebase and apply this pattern to every module that mixes pure logic with impure operations. The more code you move into prime‑testable functions, the easier your tests become. Over time, you'll develop an instinct for spotting what can be extracted.
Conclusion and Tips
Making your code prime testable is not always possible—some functions genuinely need side effects. But you can almost always reduce the amount of impure code by pushing the pure parts to the edges.
- Start small: Pick one function that's giving you testing headaches and refactor it. You'll see immediate benefits.
- Use dependency injection: For unavoidable side effects, pass in dependencies (like a database handler) so you can swap them with mocks during tests.
- Keep functions focused: A function that does one thing (especially a pure transformation) is easier to test than a monolithic one.
- Embrace functional programming techniques: Think in terms of input → output. Avoid hidden state.
Remember, every prime‑testable function you create is a victory. You'll write more tests, your confidence will grow, and your code will become more robust. Start applying these steps today, and watch your testing experience transform.
Related Articles
- Go Team Launches 2025 Developer Survey, Seeks Global Input on Language Evolution
- Go 1.25 Flight Recorder: A New Diagnostic Power Tool
- Meta's New Canary Framework Reinforces Configuration Safety Amid AI Speed Surge
- How the Python Packaging Council Was Born: A Step-by-Step Guide to PEP 772
- Go 1.26 Unveils Source-Level Inliner: A Self-Service Modernization Breakthrough for Developers
- Mastering GDB's Source-Tracking Breakpoints: A Complete Guide
- CD Projekt Red's Warsaw Studio Gains Architectural Recognition
- Solving Cloud Email Delivery Issues with Brevo's HTTP API