⌘ Clean CodeQuick reference ↗
Your developer knowledge baseBased on your Clean Code notes · Python
The practical guide

Write code that's
easy to change.

Less memorizing. More recognizing. A visual, searchable reference to the essential Clean Code principles you use every day.

Start here

Six habits that do the heavy lifting

These habits reinforce multiple rules from your notes.

Visual memory

The Clean Code mind map

Seven related questions. Use these as mental triggers, not as another set of rules to memorize.

Clean CodeRead · Focus · Trust · Test · Change · Protect · Measure
⌘
01 READNames, simple flow, cohesive modules
02 FOCUSOne job, separate concerns, useful DRY
03 TRUSTExplicit types, validated data, clear errors
04 TESTBehavior, boundaries, independent tests
05 CHANGESmall refactors, composition, measured abstraction
06 PROTECTUntrusted input, secrets, safe failures
07 MEASUREProfile first; optimize verified bottlenecks
Keep this open while coding

Quick-reference cheat sheet

Search for a principle or filter by theme. Every row connects a good habit to a code smell.

TopicDo thisWatch out for
No matching rules. Try a broader search.
Learn by seeing

Visual explanations

Eight diagrams for concepts where relationships, choices, and execution order matter more than long paragraphs. Each links to its original detailed notes.

01 · One function, one meaningful job

Split business decisions from external side effects.

process_order()
validate + calculate + save + print
Refactor into meaningful steps ↓
validate_order()
→
calculate_total()
→
save_order()

Remember: split different responsibilities, not every line.

Read function design →

02 · Flatten nested control flow

Reject invalid cases early. Keep the successful path visible.

Input
→
Valid?
No ↓
Return / raise
Yes ↓
Main action

Remember: guard clauses reduce nesting without changing intended behavior.

Read control flow →

03 · Validate → operate → handle errors

Untrusted data gets checked before important side effects.

API / user / file
→
Parse + validate
→
Domain operation
Invalid
Precise error
Expected operation failure
Recover or report

Remember: catch only what this layer can handle meaningfully.

Read validation and errors →

04 · Application layers

A thin coordinator connects business logic and external systems.

Presentation · Request / response
↓ delegates
Application · Orchestrates use case
↓ business rule
Domain · Calculation
↓ external work
Infrastructure · DB / API

Remember: keep business rules independent of technology. Layers are conceptual, not mandatory folders.

Read separation of concerns →

05 · Choose a Python data shape

Ask what the data represents before inventing a class.

What kind of data do I have?
Dynamic keys
dict
Known JSON shape
TypedDict
Domain concept
dataclass
Fixed choices
Enum / Literal
Meaningful primitive
Value object
Behavioral dependency
Protocol

Remember: stronger modeling should prevent a real mistake, not create ceremony.

Read typing and models →

06 · Testing pyramid and AAA

Focus most tests on fast, stable behavior checks.

E2E · Few
Integration · Some
Unit · Many
Arrange
→
Act
→
Assert

Remember: verify observable outcomes rather than implementation details.

Read testing →

07 · DRY: repetition is not always duplication

Extract repeated knowledge when rules truly mean the same thing.

Same business rule

Discount rate copied to web + store
↓ centralize
One discount policy

Similar-looking code

Customer name validation
Product name validation
May evolve separately

Remember: remove duplicate knowledge, not every matching line.

Read DRY and abstraction →

08 · Safe refactoring loop

Improve structure without intentionally changing observed behavior.

Understand
→
Verify tests
→
Small change
Run tests
→
Review result
↺
Repeat

Remember: adding validation changes behavior; keep that separate from structural refactors.

Read refactoring →
Fast overview

Topic library

Open a topic to see the practical rules and one question to ask yourself. Matches your notes' progression. Open the detailed library below for the full explanations and examples.

Reference · Complete source coverage

Deep-dive library

Keep the quick overview short. Expand a topic, then only the specific rule you need. All substantive text, examples, cautions, and final checklists from your supplied notes are retained below. Original code blocks are reproduced as written; indentation may be missing from the text attachment.

01

Overview

A well-designed function should be:

  • Focused
  • Clearly named
  • Predictable
  • Easy to test
  • Free from surprising side effects
1. Give a function one clear responsibility

A function should perform one meaningful job.

Python
def calculate_subtotal(unit_price, quantity):
return unit_price * quantity

Avoid combining input, calculations, file writing, database operations, and printing in one function.

A helpful question is:

Can I describe the function without using the word “and”?

If not, it may be doing too much.

2. Use intention-revealing names

Function names should normally start with an action:

Python
calculate_total()
find_customer()
validate_email_address()
save_order()
format_report()

Boolean functions should sound like questions:

Python
is_valid_quantity()
has_permission()
can_access_account()

Avoid vague names such as:

Python
process()
handle()
check()
do_stuff()
3. Keep functions focused, not artificially tiny

There is no strict line limit. Split a function when it:

  • Has unrelated stages
  • Mixes business logic with input/output
  • Has deeply nested conditions
  • Requires section comments
  • Has many local variables
  • Is difficult to test independently

Do not create separate functions for every individual line. Each function should represent a meaningful operation.

4. Keep one level of abstraction

High-level functions should read like workflows:

Python
def register_customer(customer):
validate_customer(customer)
save_customer(customer)
send_welcome_email(customer)

Low-level details, such as database statements, should be moved into lower-level functions.

5. Limit and clarify parameters

Prefer a small number of meaningful parameters:

Python
def calculate_total(subtotal, tax_rate):
return subtotal * (1 + tax_rate)

Use keyword arguments when calls might otherwise be unclear:

Python
calculate_total(
subtotal=100.0,
tax_rate=0.21,
)

Group parameters into a data class only when they represent a genuine concept, such as Customer or Product.

6. Be careful with Boolean flags

Boolean flags can hide multiple behaviors:

Python
generate_report(data, save_to_file=True)

Separate functions may be clearer:

Python
report = generate_report(data)
save_report(report, file_path)

A Boolean containing actual business data, such as is_premium_customer, is normally fine.

7. Return predictable results

Avoid returning unrelated types:

Python
# Avoid: returns a string or number
def calculate_total(items):
if not items:
return "No items"

return sum(items)

Choose a consistent contract:

Python
def calculate_total(items):
if not items:
return 0.0

return sum(items)

Alternatively, raise an exception if empty input is invalid.

8. Separate queries from commands

A query returns information:

Python
def calculate_balance(transactions):
return sum(transaction.amount for transaction in transactions)

A command changes state:

Python
def save_customer(customer):
customer_repository.save(customer)

Avoid hiding state changes inside functions named get_, find_, or calculate_.

9. Make dependencies explicit

Avoid hidden dependencies on changing global values:

Python
def calculate_total(subtotal, tax_rate):
return subtotal * (1 + tax_rate)

Passing the dependency explicitly makes the function easier to understand and test.

A module-level constant is fine when the value is genuinely fixed.

10. Make mutation and side effects obvious

Common side effects include:

  • Modifying an input object
  • Updating global state
  • Writing files
  • Changing a database
  • Sending a network request
  • Printing output

If a function modifies its input, make that behavior clear:

Python
def apply_discount_in_place(items, discount_rate):
...

Prefer returning a new result when mutation is unnecessary.

11. Return values instead of printing

Core logic should usually return a result:

Python
def calculate_total(price, quantity):
return price * quantity

The caller can then decide whether to print, save, or send it:

Python
total = calculate_total(20.0, 3)
print(total)

This keeps business logic reusable and testable.

12. Use guard clauses

Handle invalid and special cases early:

Python
def calculate_discounted_total(
subtotal,
is_premium_customer,
):
if subtotal <= 0:
raise ValueError("Subtotal must be positive.")

if not is_premium_customer:
return subtotal

return subtotal * 0.9

Guard clauses reduce nesting and keep the main execution path clear.

Quick Checklist

Before completing a function, ask:

Plain Text
[ ] Does the name clearly describe the purpose?
[ ] Does the function have one responsibility?
[ ] Does it operate at one level of abstraction?
[ ] Are its parameters necessary and understandable?
[ ] Are dependencies explicit?
[ ] Are side effects and mutations obvious?
[ ] Does it return a predictable type?
[ ] Does it avoid unnecessary printing and file access?
[ ] Can it be tested independently?
[ ] Will another developer understand it quickly?
Main takeaway

A clean function has a clear name, one meaningful responsibility, explicit inputs, predictable outputs, and no surprising side effects.

The next clean-code topic is simple control flow, covering guard clauses, early returns, nesting, complex conditions, loops, and readable comprehensions.

02

1. Use guard clauses

Handle invalid and special cases early to reduce nesting.

Python
def calculate_discount(user, subtotal):
if user is None:
return 0.0

if not user.is_active:
return 0.0

if subtotal <= 0:
return 0.0

return subtotal * 0.10
``

Guard clauses are useful for missing data, invalid arguments, unsupported states, and permission checks.

2. Remove unnecessary else blocks

After return, raise, break, or continue, an else is usually unnecessary.

Python
def get_status(is_active):
if is_active:
return "active"

return "inactive"

This keeps the code flatter and easier to scan.

3. Keep conditions direct

Prefer clear, positive conditions:

Python
if user.is_active:
grant_access()

Avoid double negatives:

Python
if not user.is_inactive:
grant_access()

Negative conditions are still useful in guard clauses:

Python
if not user.has_permission:
raise PermissionError("Access denied.")
4. Name complex conditions

Move complicated business rules into descriptive Boolean functions:

Python
def can_place_order(customer):
return (
customer.is_active
and customer.age >= 18
and customer.email_is_verified
and not customer.is_suspended
)

Usage becomes easy to understand:

Python
if can_place_order(customer):
approve_order()
5. Check Booleans directly
Python
if user.is_active:
process_user(user)

Avoid:

Python
if user.is_active == True:
process_user(user)

Use explicit checks only if True, False, and None have different meanings.

6. Use truthiness carefully

For empty collections:

Python
if not orders:
return []

If None and an empty collection have different meanings, handle them separately:

Python
if orders is None:
raise ValueError("Orders were not loaded.")

if not orders:
return []

7. Simplify range and membership checks

Use chained comparisons:

Python
if 18 <= age <= 65:
approve_application()

Use membership checks instead of repeated comparisons:

Python
if status in {"pending", "processing"}:
monitor_order()

Avoid this common bug:

Python
if status == "pending" or "processing":
...

The condition is always true because "processing" is a non-empty string.

8. Iterate directly over collections

Avoid unnecessary indexing:

Python
for customer in customers:
print(customer.name)

Use enumerate() when you need the position:

Python
for position, customer in enumerate(customers, start=1):
print(f"{position}. {customer.name}")

Use zip() for related collections:

Python
for product_name, product_price in zip(
product_names,
product_prices,
strict=True,
):
print(product_name, product_price)
9. Use continue and early return

Use continue to skip invalid loop items and keep the main action visible:

Python
for order in orders:
if not order.is_active:
continue

if order.quantity <= 0:
continue

process_order(order)

When searching for one item, return as soon as it is found:

Python
def find_customer(customers, customer_id):
for customer in customers:
if customer.id == customer_id:
return customer

return None

10. Use any() and all()

Use any() when at least one item must match:

Python
has_active_customer = any(
customer.is_active
for customer in customers
)

Use all() when every item must match:

Python
all_orders_paid = all(
order.is_paid
for order in orders
)

Remember:

Python
any([]) # False
all([]) # True
`

Check whether that empty-collection behavior matches the business requirement.

11. Keep comprehensions simple

Use comprehensions for clear filtering and transformation:

Python
active_customer_names = [
customer.name
for customer in customers
if customer.is_active
]

Use a normal loop when the logic contains:

  • Multiple steps
  • Side effects
  • Complex conditions
  • Several branches

Do not use comprehensions only for side effects:

Python
# Avoid
[send_email(customer) for customer in customers]

Use:

Python
for customer in customers:
send_email(customer)
12. Avoid complicated ternary expressions

A simple conditional expression is fine:

Python
status = "active" if user.is_active else "inactive"

For multiple conditions, use a normal function:

Python
def get_user_status(user):
if user.is_premium:
return "premium"

if user.is_active:
return "active"

return "inactive"

13. Use mappings for simple value selection

Instead of repeated branches:

Python
STATUS_MESSAGES = {
"pending": "The order is waiting.",
"processing": "The order is being processed.",
"completed": "The order is complete.",
}

def get_status_message(status):
return STATUS_MESSAGES.get(status, "Unknown status.")

Use mappings when one known value directly selects another value. Keep conditionals when each branch performs different actions.

Quick Checklist

Plain Text
[ ] Are invalid cases handled early?
[ ] Is nesting kept shallow?
[ ] Can unnecessary else blocks be removed?
[ ] Are conditions direct and understandable?
[ ] Do complex conditions have meaningful names?
[ ] Are Booleans checked directly?
[ ] Is truthiness used correctly?
[ ] Are range and membership checks concise?
[ ] Do loops iterate directly over objects?
[ ] Would enumerate(), zip(), any(), or all() help?
[ ] Are comprehensions short and free from side effects?
[ ] Are ternary expressions easy to understand?
[ ] Would a mapping be clearer than repeated branches?
Main takeaway

Clean control flow handles invalid cases early, minimizes nesting, and keeps the normal execution path easy to see.

03

1. Understand the difference

Validation checks whether data follows your application’s rules:

Python
if quantity <= 0:
raise ValueError("Quantity must be greater than zero.")

Error handling decides how the application responds when an operation fails:

Python
try:
quantity = int(user_input)
except ValueError:
print("Enter a valid whole number.")
2. Validate data at system boundaries

Validate untrusted data when it enters your application, including:

  • User input
  • Files
  • API requests and responses
  • Database records
  • Environment variables
  • Command-line arguments
Python
def parse_quantity(raw_quantity: str) -> int:
try:
quantity = int(raw_quantity)
except ValueError as error:
raise ValueError(
"Quantity must be a whole number."
) from error
  • if quantity <= 0:
  • raise ValueError(
  • "Quantity must be greater than zero."
  • )

return quantity

Core business functions should also protect critical rules that must never be bypassed.

3. Fail early

Validate inputs before calculations, file writes, database updates, or other side effects:

Python
def calculate_total(
unit_price: float,
quantity: int,
) -> float:
if unit_price < 0:
raise ValueError("Unit price cannot be negative.")
  • if quantity <= 0:
  • raise ValueError(
  • "Quantity must be greater than zero."
  • )

return unit_price * quantity

Invalid data should not be allowed to travel deeper into the application.

4. Write specific error messages

Avoid vague messages:

Python
raise ValueError("Invalid input.")

Prefer messages that identify the problem and expectation:

Python
raise ValueError(
"Discount rate must be between 0 and 1."
)

A useful message explains:

  • Which value is invalid
  • Which rule was violated
  • What value or format is expected

Do not expose credentials or sensitive internal details.

5. Catch specific exceptions

Avoid broad or bare exception handlers:

Python
try:
quantity = int(user_input)
except:
print("Something went wrong.")

Catch only the expected exception:

Python
try:
quantity = int(user_input)
except ValueError:
print("Quantity must be a whole number.")

This prevents unrelated programming errors from being hidden.

6. Keep try blocks small

Only include the operation expected to fail:

Python
try:
quantity = int(user_input)
except ValueError:
print("Quantity must be a whole number.")
else:
total = calculate_total(price, quantity)
save_order(total)

A small try block makes it clear which operation the handler belongs to.

7. Catch errors only when you can respond meaningfully

A function should catch an exception only when it can:

  • Recover safely
  • Retry safely
  • Use a valid fallback
  • Add useful context
  • Convert it into a domain-specific error
  • Display an appropriate message at the application boundary

If it cannot do one of these, let the exception propagate.

Never silently ignore important failures:

Python
# Avoid
try:
save_customer(customer)
except DatabaseError:
pass
8. Use exception chaining

When converting a technical exception into a clearer application exception, preserve the original cause:

Python
class InvalidQuantityError(ValueError):
"""Raised when an order quantity is invalid."""
  • def parse_quantity(raw_quantity: str) -> int:
  • try:
  • return int(raw_quantity)
  • except ValueError as error:
  • raise InvalidQuantityError(
  • "Quantity must be a whole number."
  • ) from error

The application receives a meaningful error while developers retain the original debugging information.

9. Create custom exceptions only when useful

Built-in exceptions are usually enough for small programs:

  • ValueError
  • TypeError
  • KeyError
  • FileNotFoundError

Use custom exceptions when callers need to distinguish domain-specific failures:

Python
class OrderError(Exception):
"""Base exception for order failures."""

class InvalidOrderError(OrderError):
"""Raised when an order violates a business rule."""

class OrderNotFoundError(OrderError):
"""Raised when an order cannot be found."""

Do not create a separate custom exception for every minor failure.

10. Choose intentionally between None and exceptions

Return None when absence is normal and expected:

Python
def find_customer(customers, customer_id):
for customer in customers:
if customer.id == customer_id:
return customer

return None

Raise an exception when the requested operation cannot proceed:

Python
def get_customer(customers, customer_id):
customer = find_customer(customers, customer_id)
  • if customer is None:
  • raise CustomerNotFoundError(
  • f"Customer {customer_id} was not found."
  • )

return customer

A useful convention is:

find_... may return None.
get_... expects the result to exist and may raise an exception.

11. Do not use exceptions for normal control flow

If a missing dictionary value is expected, use .get():

Python
customer = customers.get(customer_id)

If no orders is a normal condition:

Python
if not orders:
return 0.0

Exceptions should represent situations that prevent normal completion, not every alternative outcome.

12. Manage resources safely

Use context managers for files and similar resources:

Python
with open(file_path, encoding="utf-8") as order_file:
order = json.load(order_file)

Use finally when cleanup must always happen and no context manager is available:

Python
connection = create_connection()
  • try:
  • process_orders(connection)
  • finally:
  • connection.close()
13. Use clear validation contracts

A validation function should normally either return a valid value or raise an exception:

Python
def validate_discount_rate(
discount_rate: float,
) -> float:
if not 0 <= discount_rate <= 1:
raise ValueError(
"Discount rate must be between 0 and 1."
)

return discount_rate

Use naming to clarify behavior:

validate_order() raises when invalid.
is_valid_order() returns True or False.

14. Separate user messages from diagnostic details

Users need a clear message:

Plain Text
The order could not be saved. Please try again.

Developers need technical details in logs:

Python
try:
save_order(order)
except DatabaseError:
logger.exception("Failed to save order %s", order.id)
display_error(
"The order could not be saved. Please try again."
)

Avoid showing users tracebacks, database details, credentials, or internal paths.

Quick Checklist

Plain Text
[ ] Is external input validated at the boundary?
[ ] Are important business rules protected?
[ ] Does validation happen before side effects?
[ ] Are error messages specific and actionable?
[ ] Are only expected exceptions caught?
[ ] Are try blocks small and focused?
[ ] Can this layer genuinely handle the error?
[ ] Is exception chaining used when translating errors?
[ ] Is the choice between None and an exception intentional?
[ ] Are resources cleaned up safely?
[ ] Are user messages separated from technical details?
[ ] Are failures prevented from continuing silently?
Main takeaway

Validate data before using it, raise precise exceptions when processing cannot continue, and catch errors only where you can recover or report them meaningfully.

04

Overview

Separation of concerns means keeping different responsibilities in different parts of your application.

Common concerns include:

  • User input and output
  • Validation
  • Business rules
  • Formatting
  • File and database access
  • External API calls
  • Logging and notifications
  • Application coordination
1. Separate business logic from input and output

Avoid mixing input(), calculations, file writing, and print() in one function.

Python
def calculate_order_total(
unit_price: float,
quantity: int,
) -> float:
if unit_price < 0:
raise ValueError("Unit price cannot be negative.")

if quantity <= 0:
raise ValueError("Quantity must be positive.")

return unit_price * quantity

The application boundary handles communication with the user:

Python
def main() -> None:
unit_price = float(input("Unit price: "))
quantity = int(input("Quantity: "))

total = calculate_order_total(unit_price, quantity)

print(f"Order total: €{total:.2f}")

The calculation can now be reused by a console application, website, API, or test.

2. Understand the main application layers

A small application commonly has four conceptual layers:

Presentation

Receives input and presents output:

Python
def display_total(total: float) -> None:
print(f"Order total: €{total:.2f}")
Application

Coordinates a use case:

Python
def create_order(
unit_price,
quantity,
repository,
):
total = calculate_order_total(
unit_price,
quantity,
)
repository.save(total)

return total
Domain

Contains calculations and business rules:

Python
def calculate_order_total(
unit_price,
quantity,
):
return unit_price * quantity
Infrastructure

Communicates with external systems:

Python
def save_total(total, file_path):
with open(
file_path,
"a",
encoding="utf-8",
) as output_file:
output_file.write(f"{total:.2f}\n")

You do not always need separate folders or classes for every layer. The important part is keeping their responsibilities distinct.

3. Separate calculation from formatting

Keep business results in their useful data type:

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

Format the result separately:

Python
def format_currency(
amount: float,
symbol: str = "€",
) -> str:
return f"{symbol}{amount:.2f}"

Do not return formatted text from a calculation if callers may need the numeric value later.

4. Isolate storage and network operations

Business rules should not directly depend on:

  • Files
  • Databases
  • HTTP requests
  • Email services
  • Cloud storage

Instead of combining calculation and storage:

Python
total = calculate_order_total(items)
save_order_total(total, file_path)

This makes the calculation testable without creating files or connecting to external systems.

5. Make dependencies explicit

Dependency injection means passing external dependencies into a function instead of secretly constructing them inside it.

Python
def save_order(order, repository) -> None:
repository.save(order)

Production can use a database repository:

Python
save_order(order, database_repository)

Tests can use an in-memory repository:

Python
save_order(order, in_memory_repository)

Inject dependencies when they perform external work, vary by environment, or need safe substitutes during testing.

Do not inject every small pure helper. That creates unnecessary complexity.

6. Keep pure logic at the core

A pure function:

  • Uses only its inputs
  • Returns a result
  • Does not modify external state
  • Produces the same result for the same inputs
Python
def calculate_tax(
subtotal: float,
tax_rate: float,
) -> float:
return subtotal * tax_rate

Impure functions interact with the outside world:

Python
def save_invoice(invoice, repository) -> None:
repository.save(invoice)

A useful structure is:

  • Read the data
  • Transform it with pure functions
  • Write or display the result

This keeps most business logic easy to test.

7. Use a thin coordination layer

The application layer should connect the steps without containing all implementation details:

Python
def create_order(
items,
customer,
repository,
):
total = calculate_order_total(
items,
customer.is_premium,
)
  • repository.save(
  • customer_id=customer.id,
  • total=total,
  • )

return total

This function coordinates the use case while calculations and storage remain separate.

8. Avoid unnecessary abstraction

Separation of concerns does not mean:

  • One class for every function
  • One file for every tiny operation
  • Interfaces for every helper
  • Splitting coherent calculations into meaningless pieces

Separate responsibilities when they:

  • Change for different reasons
  • Depend on different technologies
  • Need independent tests
  • Have distinct business meanings
  • Can be reused independently
  • Quick Checklist
Plain Text
[ ] Is business logic independent of input and output?
[ ] Are calculations separate from formatting?
[ ] Are files, databases, and APIs isolated?
[ ] Does the application layer coordinate rather than contain details?
[ ] Are external dependencies explicit?
[ ] Can business rules be tested without external systems?
[ ] Are side effects kept near the application boundary?
[ ] Can one responsibility change without affecting unrelated code?
[ ] Do abstractions solve a real problem?
Main takeaway

Keep business rules at the core, external interactions at the edges, and use a thin application layer to coordinate the workflow.

05

Overview

Automated tests verify that code continues to behave as expected when you change or refactor it. They reduce regressions, document behavior, and improve confidence, but they only check scenarios you actually write.

1. Design code to be testable

Functions are easiest to test when they:

  • Accept explicit inputs
  • Return results
  • Avoid hidden global state
  • Keep side effects separate
  • Do not directly depend on input(), print(), files, databases, or networks
Python
def calculate_total(
unit_price: float,
quantity: int,
) -> float:
return unit_price * quantity

Core logic should return values. Application-level code can print, save, or send those values.

2. Follow Arrange, Act, Assert

A clear test usually has three stages:

Python
def test_calculate_total_multiplies_price_by_quantity():
unit_price = 20.0
quantity = 3

result = calculate_total(unit_price, quantity)

  • assert result == 60.0
  • Arrange: Prepare inputs and dependencies
  • Act: Execute the behavior
  • Assert: Verify the result
3. Test behavior, not implementation

Tests should verify the function’s public promise, not private variables or the exact internal sequence.

If you refactor the internal code without changing its behavior, good tests should normally continue to pass.

4. Use descriptive test names

A test name should describe:

  • What is being tested
  • The scenario
  • The expected outcome
Python
def test_calculate_total_returns_zero_for_free_item():
...

def test_parse_quantity_rejects_non_numeric_input():
...

def test_premium_customer_receives_discount():
...

Avoid names such as test_1() or test_function().

5. Test different categories of behavior

Cover at least:

  • Normal cases
  • Boundary values
  • Empty input
  • Invalid input
  • Expected exceptions
  • Important business rules
  • External dependency failures when relevant
Python
def test_calculate_total_multiplies_valid_values():
assert calculate_total(20.0, 3) == 60.0
Python
def test_calculate_total_allows_zero_price():
assert calculate_total(0.0, 3) == 0.0
Python
def test_calculate_total_rejects_zero_quantity():
with pytest.raises(
ValueError,
match="greater than zero",
):
calculate_total(20.0, 0)
6. Keep each test focused

Each test should normally verify one behavior.

Prefer separate tests for:

  • Subtotal calculation
  • Tax calculation
  • Discount calculation
  • Validation failures

Focused tests make failures easier to diagnose.

7. Keep tests independent

Tests should not depend on:

  • Execution order
  • Data created by another test
  • Shared mutable global state
  • Production databases
  • Real network services
  • Real user accounts

Each test should arrange its own data and clean up its own resources.

8. Use parametrization for repeated scenarios

Use pytest.mark.parametrize when the same behavior should be checked with several inputs:

Python
@pytest.mark.parametrize(
("unit_price", "quantity", "expected"),
[
(10.0, 1, 10.0),
(10.0, 3, 30.0),
(0.0, 5, 0.0),
],
)
def test_calculate_total(
unit_price,
quantity,
expected,
):
assert calculate_total(unit_price, quantity) == expected

Use separate tests when scenarios require different setup or communicate different business rules.

9. Use fixtures for reusable setup

Fixtures provide reusable test data or resources:

Python
@pytest.fixture
def premium_customer():
return Customer(
name="Pranoy",
is_premium=True,
)

Fixtures are useful for:

  • Reusable objects
  • Temporary resources
  • Test databases
  • Shared configuration
  • Setup and cleanup

Keep fixtures understandable. Do not hide important test data inside complex fixture chains.

10. Isolate external dependencies

Tests should not require real files, databases, APIs, or email services unless they are deliberate integration tests.

Use simple test doubles:

Python
class InMemoryOrderRepository:
def __init__(self):
self.saved_orders = []

def save(self, order):
self.saved_orders.append(order)

Then test the outcome:

Python
def test_create_order_saves_order():
repository = InMemoryOrderRepository()
order = Order(...)

create_order(order, repository)

assert repository.saved_orders == [order]

Prefer a small fake implementation when it is clearer than a mock.

11. Avoid excessive mocking

Mock external boundaries, not every internal function.

Too many mocks make tests dependent on implementation details and cause them to fail during harmless refactoring.

Prefer checking observable outcomes:

  • Returned result
  • Saved data
  • Changed state
  • Raised exception
  • Produced external action
12. Keep tests simpler than production code

Avoid recreating the implementation inside the test:

Python
# Less useful
expected = price * quantity
assert calculate_total(price, quantity) == expected

Prefer explicit expected values:

Python
assert calculate_total(20.0, 3) == 60.0

If the test repeats the same mistake as the production code, it may pass incorrectly.

13. Use coverage as a guide

Coverage shows which lines or branches ran during tests, but a high percentage does not guarantee useful tests.

Use coverage to find untested areas, not as the only quality measure.

Shell
pytest --cov=order_app
14. Balance the test suite

A healthy test suite typically contains:

  • Many fast unit tests
  • Fewer integration tests
  • A small number of end-to-end tests

Unit tests provide fast feedback. Integration and end-to-end tests confirm that components work together.

Quick Testing Checklist

Plain Text
[ ] Does every test verify one behavior?
[ ] Are test names descriptive?
[ ] Are normal, boundary, and invalid cases covered?
[ ] Do tests follow Arrange, Act, Assert?
[ ] Are tests independent?
[ ] Are expected values explicit?
[ ] Are important exceptions tested?
[ ] Are external dependencies isolated?
[ ] Are mocks used only when necessary?
[ ] Do tests focus on public behavior?
[ ] Is coverage used to find gaps rather than chase a percentage?
[ ] Can the test suite run quickly and consistently?
Main takeaway

Test public behavior, keep tests focused and independent, isolate external systems, and use the test suite as protection during refactoring.

06

1. Define clear function contracts

Type hints communicate what a function accepts and returns:

Python
def calculate_total(
unit_price: float,
quantity: int,
) -> float:
return unit_price * quantity

Type hints improve readability, editor support, and static analysis. They do not enforce types at runtime.

2. Annotate important boundaries

Prioritize type hints for:

  • Public functions and methods
  • Module interfaces
  • External dependencies
  • Complex data structures
  • Values that can be missing

Local variables usually do not need annotations when their type is obvious:

Python
subtotal = unit_price * quantity
`

Add a local annotation when it improves clarity:

Python
orders_by_id: dict[int, Order] = {}
3. Specify collection contents

Avoid broad collection annotations:

Python
def calculate_total(prices: list) -> float:
return sum(prices)

Specify the element type:

Python
def calculate_total(prices: list[float]) -> float:
return sum(prices)

Common examples:

Python
customer_names: list[str]
coordinates: tuple[float, float]
orders_by_id: dict[int, Order]
supported_statuses: set[str]
4. Accept the least specific useful type

If a function only iterates through values, accept Iterable rather than requiring a list:

Python
from collections.abc import Iterable
  • def calculate_total(
  • prices: Iterable[float],
  • ) -> float:
  • return sum(prices)

Use Sequence when indexing or length is required.

The input type should describe the capabilities that the function genuinely needs.

5. Make optional values explicit

If a function may return None, show it in the annotation:

Python
def find_customer(
customers: list[Customer],
customer_id: int,
) -> Customer | None:
...

The caller must handle the missing case:

Python
customer = find_customer(customers, customer_id)

if customer is None:
return

send_notification(customer)

After the check, the type checker understands that customer is a Customer.

6. Avoid excessive use of Any

This contract provides little protection:

Python
def process(data: Any) -> Any:
...

Prefer a precise contract:

Python
def calculate_order_total(
items: list[OrderItem],
) -> float:
...

Use Any mainly for genuinely dynamic data at external boundaries, then convert that data into a structured type as early as possible.

7. Keep unions focused

A union is useful when multiple types are genuinely supported:

Python
def normalize_customer_id(
customer_id: int | str,
) -> str:
return str(customer_id)

Very broad unions can indicate unclear responsibilities:

Python
str | int | float | list | dict | None

Normalize external data early so the core application works with predictable types.

8. Model structured data clearly

Instead of passing loosely defined dictionaries:

Python
def calculate_item_total(item: dict) -> float:
return item["price"] * item["quantity"]

Use a data class:

Python
from dataclasses import dataclass
  • @dataclass
  • class OrderItem:
  • name: str
  • unit_price: float
  • quantity: int

Then:

Python
def calculate_item_total(item: OrderItem) -> float:
return item.unit_price * item.quantity

Use TypedDict when the data must remain dictionary-shaped, such as near JSON or API boundaries.

9. Represent fixed choices explicitly

Use Literal for a small set of accepted values:

Python
from typing import Literal
  • OrderStatus = Literal[
  • "pending",
  • "processing",
  • "completed",
  • ]

Use an Enum when those values are important domain concepts:

Python
from enum import Enum
  • class OrderStatus(Enum):
  • PENDING = "pending"
  • PROCESSING = "processing"
  • COMPLETED = "completed"

Runtime validation is still required for data received from users, files, or APIs.

10. Use protocols for important dependencies

A protocol describes required behavior:

Python
from typing import Protocol
  • class OrderRepository(Protocol):
  • def save(self, order: Order) -> None:
  • ...

A function can then depend on the capability rather than a specific technology:

Python
def create_order(
order: Order,
repository: OrderRepository,
) -> None:
repository.save(order)

Protocols are helpful for replaceable boundaries such as repositories, API clients, and notification services. Do not create them for every small helper.

11. Annotate no-return-value functions correctly

Use -> None when a function performs an action without returning a meaningful result:

Python
def save_order(order: Order) -> None:
repository.save(order)

Use Never only when a function cannot complete normally:

Python
from typing import Never

def fail(message: str) -> Never:
raise RuntimeError(message)

12. Ensure annotations match reality

Do not claim a function always returns an object when it can return None.

Incorrect:

Python
def find_customer(customer_id: int) -> Customer:
return customers.get(customer_id)

Correct:

Python
def find_customer(
customer_id: int,
) -> Customer | None:
return customers.get(customer_id)

Alternatively, raise an exception and preserve the stronger return contract.

13. Type hints do not replace validation or tests

A type hint can declare that discount_rate is a float:

Python
def apply_discount(discount_rate: float) -> None:
...

It cannot guarantee that the value is valid.

Runtime validation is still required:

Python
if not 0 <= discount_rate <= 1:
raise ValueError(
"Discount rate must be between 0 and 1."
)

Remember:

Type hints describe kinds of values.
Validation enforces allowed values and business rules.
Tests verify runtime behavior.

14. Check types automatically

Run ty from the project directory:

Shell
ty check

A useful development workflow is:

Shell
ruff format .
ruff check .
ty check
pytest

Each tool answers a different question:

  • Ruff: Is the code formatted and free from common lint issues?
  • ty: Do declared and actual types agree?
  • pytest: Does tested runtime behavior work correctly?
  • Developer review: Is the solution understandable, correct, and maintainable?
  • Quick Checklist
Plain Text
[ ] Are public function inputs and outputs annotated?
[ ] Are collection element types specified?
[ ] Are optional results marked with | None?
[ ] Is Any avoided where a more precise type is possible?
[ ] Are unions limited to genuinely supported types?
[ ] Do annotations match actual behavior?
[ ] Are fixed choices represented clearly?
[ ] Would a data class clarify structured internal data?
[ ] Would TypedDict clarify dictionary-shaped boundary data?
[ ] Would a protocol clarify an important dependency?
[ ] Are type hints supported by runtime validation?
[ ] Does the type checker pass without unnecessary suppressions?
Main takeaway

Type hints define what kinds of data move through the program. Validation determines whether the actual values are acceptable.

07

Overview

Clear data modeling means representing business concepts explicitly, so valid data is easy to create and invalid states are difficult to represent.

1. Choose the right structure

Use the structure that best represents the data:

  • dict: dynamic key-value data
  • TypedDict: a known dictionary shape, often for JSON or API data
  • dataclass: a meaningful internal business concept
  • Enum: a fixed set of choices
  • Value object: an important primitive with validation or behavior
  • Protocol: a behavioral contract for replaceable dependencies

Avoid passing loosely structured dictionaries throughout the core application.

2. Use data classes for meaningful concepts

Instead of:

Python
item = {
"product_name": "Keyboard",
"unit_price": 75.0,
"quantity": 2,
}

Use:

Python
from dataclasses import dataclass
  • @dataclass
  • class OrderItem:
  • product_name: str
  • unit_price: float
  • quantity: int

This makes required fields and expected types visible and reduces mistakes caused by misspelled dictionary keys.

3. Keep related behavior with the model

Behavior that depends mainly on an object’s data can belong to that object:

Python
@dataclass
class OrderItem:
product_name: str
unit_price: float
quantity: int

def calculate_total(self) -> float:
return self.unit_price * self.quantity

Do not add unrelated responsibilities, such as database access or sending emails, to a simple data model.

4. Prevent invalid objects

Validate important rules when the object is created:

Python
@dataclass
class OrderItem:
product_name: str
unit_price: float
quantity: int
  • def __post_init__(self) -> None:
  • if not self.product_name.strip():
  • raise ValueError("Product name cannot be empty.")

if self.unit_price < 0:
raise ValueError("Unit price cannot be negative.")

if self.quantity <= 0:
raise ValueError("Quantity must be positive.")

If an object should never exist in an invalid state, reject invalid data during construction.

5. Normalize values carefully

Models can normalize consistently formatted values:

Python
@dataclass
class Customer:
name: str
email_address: str
  • def __post_init__(self) -> None:
  • self.name = self.name.strip()
  • self.email_address = self.email_address.strip().lower()

Only normalize when the transformation is supported by clear business rules. Do not silently alter meaningful data.

6. Use immutability where appropriate

Use frozen=True for values that should not change after creation:

Python
@dataclass(frozen=True)
class CustomerId:
value: int

Immutability is useful for:

  • Identifiers
  • Email-address value objects
  • Configuration
  • Coordinates
  • Historical records
  • Money values

Remember that frozen=True is shallow. A frozen model can still contain a mutable list. Use tuples for stronger immutability.

7. Replace conflicting Booleans with an enum

Avoid models that allow contradictory states:

Python
class Order:
is_pending: bool
is_processing: bool
is_completed: bool

Use one status:

Python
from enum import Enum
  • class OrderStatus(Enum):
  • PENDING = "pending"
  • PROCESSING = "processing"
  • COMPLETED = "completed"
  • CANCELLED = "cancelled"

An order can now have exactly one status.

8. Enforce valid state transitions

Do not allow unrestricted status changes. Use descriptive methods:

Python
def complete(self) -> None:
if self.status is not OrderStatus.PROCESSING:
raise ValueError(
"Only processing orders can be completed."
)

self.status = OrderStatus.COMPLETED

Methods such as start_processing(), complete(), and cancel() make the lifecycle visible and protect the object’s rules.

9. Use value objects for important primitives

A plain string or integer may not communicate enough meaning:

Python
@dataclass(frozen=True)
class CustomerId:
value: int
  • @dataclass(frozen=True)
  • class OrderId:
  • value: int

These types prevent accidentally passing an order ID where a customer ID is required.

Value objects are useful when a value:

  • Has validation rules
  • Has related behavior
  • Is easily confused with another primitive
  • Appears frequently in the domain
  • Should be immutable

Do not wrap every primitive automatically.

10. Model money carefully

For exact decimal financial calculations, use Decimal rather than float:

Python
from decimal import Decimal

price = Decimal("19.99")
tax_rate = Decimal("0.21")

Create Decimal values from strings, not floats.

A money model can also store currency and prevent adding different currencies:

Python
@dataclass(frozen=True)
class Money:
amount: Decimal
currency: str

Whether negative amounts are valid depends on the domain, since refunds and debts may require them.

11. Handle mutable defaults correctly

Never define a shared mutable default:

Python
# Avoid
items: list[OrderItem] = []

Use default_factory:

Python
from dataclasses import field

items: list[OrderItem] = field(default_factory=list)

Each object then receives its own list.

12. Protect internal collections when needed

If the model must control how a collection changes, expose a read-only representation and provide meaningful methods:

Python
@property
def items(self) -> tuple[OrderItem, ...]:
return tuple(self._items)

def add_item(self, item: OrderItem) -> None:
self._items.append(item)

This lets the model enforce rules around adding, removing, or changing items.

13. Convert external data at the boundary

Do not pass raw JSON or API dictionaries throughout the application.

Convert them into domain models early:

Python
def parse_order(raw_order: dict[str, str]) -> Order:
return Order(
order_id=int(raw_order["id"]),
status=OrderStatus(raw_order["status"]),
)

The core application then works with known types and validated values.

14. Prefer composition over inheritance

Build larger models from smaller concepts:

Python
@dataclass
class Customer:
name: str
email_address: EmailAddress
delivery_address: Address

Composition is often more flexible than creating deep inheritance trees such as Customer, PremiumCustomer, and CorporatePremiumCustomer.

Use inheritance only for a genuine and stable “is-a” relationship.

15. Avoid over-modeling

Not every dictionary needs a class, and not every string needs a value object.

Introduce a model when it:

  • Clarifies recurring structure
  • Centralizes validation
  • Prevents common mistakes
  • Holds related behavior
  • Represents an important business concept
  • Protects important rules

A model should reduce complexity, not add unnecessary ceremony.

08

Overview

Duplication becomes dangerous when the same business rule or knowledge exists in multiple places.

DRY principle

DRY means Don’t Repeat Yourself. It is intended to prevent multiple sources of truth.

Repeated business rule

Python
def calculate_web_discount(subtotal: float) -> float:
if subtotal >= 100:
return subtotal * 0.10

return 0.0

  • def calculate_store_discount(subtotal: float) -> float:
  • if subtotal >= 100:
  • return subtotal * 0.10

return 0.0
Centralized rule

Python
DISCOUNT_THRESHOLD = 100.0
DISCOUNT_RATE = 0.10
  • def calculate_discount(subtotal: float) -> float:
  • if subtotal < DISCOUNT_THRESHOLD:
  • return 0.0

return subtotal * DISCOUNT_RATE

Now the threshold and rate have one source of truth.

Repeated code is not always repeated knowledge

Two functions can look similar while representing different business concepts:

Python
def validate_customer_name(name: str) -> None:
if not name.strip():
raise ValueError("Customer name cannot be empty.")
  • def validate_product_name(name: str) -> None:
  • if not name.strip():
  • raise ValueError("Product name cannot be empty.")
  • ``

These rules may evolve differently later. Combining them too early could create unnecessary coupling.

Rule of Three

A practical guideline is:

Write the code the first time.
Accept some duplication the second time.
Consider abstraction when the pattern appears a third time.

You can abstract earlier when the duplicated code represents an important business rule or inconsistency would be dangerous.

Good abstractions

A good abstraction:

  • Has a meaningful domain-specific name
  • Represents stable knowledge
  • Reduces multiple sources of truth
  • Hides irrelevant implementation details
  • Is easier to understand than the duplicated code
  • Does not require many flags or modes
Warning signs of a bad abstraction

Be cautious when an abstraction has:

  • Many Boolean flags
  • Mode strings such as "create" and "update"
  • Parameters used only in certain branches
  • Frequent type checks
  • Many exceptions to the general rule
  • A vague name such as process_data()
  • Main takeaway

Remove repeated knowledge, not every repeated line. A little duplication is often better than the wrong abstraction.

09

Overview

Modules and packages give related responsibilities predictable locations.

A module is normally one Python file.
A package is a directory containing related modules.

Plain Text
order_app/
├── __init__.py
├── models.py
├── pricing.py
├── validation.py
└── main.py
Keep modules cohesive

A module should have one clear purpose.

For example, pricing.py might contain:

Python
def calculate_subtotal(items):
...

def calculate_tax(subtotal, tax_rate):
...

def calculate_discount(subtotal, discount_rate):
...

def calculate_total(items, tax_rate, discount_rate):
...

All these functions concern pricing.

Avoid placing unrelated functions in vague modules such as:

Plain Text
utils.py
helpers.py
common.py
misc.py

These names often become dumping grounds.

Organize by feature as the application grows

For a larger application, feature-based organization can be more maintainable:

Plain Text
shop/
├── customers/
│ ├── models.py
│ ├── service.py
│ └── repository.py
├── orders/
│ ├── models.py
│ ├── pricing.py
│ └── service.py
└── payments/
├── client.py
└── service.py

Code that changes together stays together.

Keep the entry point small

main() should assemble dependencies and coordinate the application:

Python
def main() -> int:
settings = load_settings()
repository = create_repository(settings)

run_application(repository)

return 0

if __name__ == "__main__":
raise SystemExit(main())

It should not contain all business rules, database queries, and formatting logic.

Control dependency direction

Core business logic should not directly depend on:

  • HTTP libraries
  • Databases
  • Files
  • User interfaces
  • Framework details

Instead, application code should coordinate business logic and infrastructure.

Avoid circular imports

Circular imports often indicate:

  • Poorly separated responsibilities
  • Shared concepts in the wrong module
  • Modules that know too much about one another
  • Artificial module boundaries

Possible solutions include:

  • Moving shared concepts to a third module
  • Moving behavior to its natural owner
  • Depending on a protocol
  • Redesigning the dependency direction
Avoid import-time side effects

Importing a module should not unexpectedly connect to databases or run workflows.

Avoid:

Python
database = connect_to_database()
customers = load_customers()

Prefer:

Python
def create_database_connection():
...

Construct the resource explicitly in main().

Main takeaway

Group related behavior, make dependencies visible, keep entry points small, and do not let core business logic depend on external technologies.

10

Overview

Refactoring means improving the internal structure of code without intentionally changing its observable behavior.

Safe refactoring workflow

Understand the existing behavior.
Add or verify tests.
Make one small structural change.
Run the tests.
Review the improvement.
Repeat.

Useful verification commands are:

Shell
ruff format .
ruff check .
ty check
pytest
Common refactoring techniques

Rename unclear identifiers

Python
# Before
def calc(p, q):
return p * q
Python
# After
def calculate_subtotal(
unit_price: float,
quantity: int,
) -> float:
return unit_price * quantity

Extract a meaningful function

Python
def calculate_subtotal(items) -> float:
return sum(
item.unit_price * item.quantity
for item in items
)
``

Extract code when it represents a business rule, separate responsibility, or reusable concept.

Inline an unnecessary function

If a function only forwards a value and contributes no meaning, remove it.

Python
# Unnecessary wrapper
def get_total(order):
return order.calculate_total()

Call the meaningful operation directly:

Python
total = order.calculate_total()
Introduce explanatory variables
Python
subtotal = unit_price * quantity
discount_amount = subtotal * discount_rate
final_total = subtotal - discount_amount

This is often clearer than one compressed expression.

Replace magic values

Python
PREMIUM_DISCOUNT_RATE = 0.10
FREE_DELIVERY_THRESHOLD = 100.0

Names should explain the business meaning.

Simplify conditions

Use:

  • Guard clauses
  • Early returns
  • Named Boolean functions
  • Membership checks
  • Chained comparisons
  • Mappings for direct value selection
  • Move behavior to its natural owner

If an operation primarily depends on one object’s state, it may belong on that object:

Python
@dataclass
class OrderItem:
unit_price: float
quantity: int
  • def calculate_total(self) -> float:
  • return self.unit_price * self.quantity
  • Remove dead and speculative code

Delete:

  • Unused functions
  • Commented-out implementations
  • Unused parameters
  • Obsolete branches
  • Unnecessary extension points
  • Unsupported future features

Version control preserves history.

Refactoring versus behavior changes

Adding validation changes behavior:

Python
if quantity <= 0:
raise ValueError("Quantity must be positive.")

That may be valuable, but it is not purely structural refactoring. Keeping these changes separate makes reviews and debugging easier.

Main takeaway

Refactor through small, test-protected changes. Improve structure without redesigning more than the current problem requires.

11

Overview

Object-oriented design combines related state and behavior in cohesive objects.

When to use a class

A class is useful when:

  • Data and behavior naturally belong together
  • Several operations share state
  • Important invariants must be protected
  • The concept has a lifecycle
  • Multiple implementations share a behavioral contract
  • The object has meaningful identity

Example:

Python
class BankAccount:
def __init__(self, balance: float = 0.0) -> None:
if balance < 0:
raise ValueError(
"Initial balance cannot be negative."
)

self._balance = balance

  • @property
  • def balance(self) -> float:
  • return self._balance
  • def deposit(self, amount: float) -> None:
  • if amount <= 0:
  • raise ValueError(
  • "Deposit amount must be positive."
  • )

self._balance += amount

The account controls its own valid state.

When a function is better

Do not create a class for a simple stateless transformation:

Python
def calculate_tax(
subtotal: float,
tax_rate: float,
) -> float:
return subtotal * tax_rate

A TaxCalculator class without meaningful state would add unnecessary ceremony.

Keep classes cohesive

A class should have one focused responsibility.

Avoid oversized manager classes containing:

  • Pricing
  • Database access
  • Email
  • File generation
  • Logging
  • Reporting

These responsibilities should normally be separated.

Encapsulation

Encapsulation means controlling how important internal state changes.

Instead of:

Python
order.status = OrderStatus.COMPLETED

Prefer:

Python
def complete(self) -> None:
if self.status is not OrderStatus.PROCESSING:
raise ValueError(
"Only processing orders can be completed."
)

self.status = OrderStatus.COMPLETED

Properties

Use a property when attribute access requires validation, calculation, or controlled exposure:

Python
@property
def balance(self) -> float:
return self._balance

Do not hide network requests or expensive operations inside properties.

Composition over inheritance

Composition assembles an object from smaller collaborators:

Python
class OrderService:
def __init__(
self,
repository,
payment_service,
notifier,
) -> None:
self._repository = repository
self._payment_service = payment_service
self._notifier = notifier

Prefer composition when components vary independently.

Use inheritance only for a genuine and stable “is-a” relationship where subclasses can replace the base type without surprising callers.

Keep constructors simple

Constructors should establish valid state. They should not unexpectedly:

  • Call remote APIs
  • Connect to databases
  • Write files
  • Send notifications
  • Run complete workflows
Avoid God objects

Warning signs include:

  • Many unrelated methods
  • Many dependencies
  • Frequent unrelated changes
  • Difficult setup in tests
  • Vague names such as ApplicationManager
  • Main takeaway

Use a class when data, behavior, and lifecycle rules form one cohesive concept. Prefer functions for simple transformations and composition for combining capabilities.

12

Overview

You chose to skip the detailed SOLID lesson, so this section is only a reference.

SOLID is a group of five object-oriented design principles:

S: Single Responsibility Principle

A component should have one focused responsibility or one main reason to change.

O: Open/Closed Principle

Code should be extensible without requiring frequent modification of stable behavior.

L: Liskov Substitution Principle

A subtype should be usable wherever its base type is expected without breaking the promised behavior.

I: Interface Segregation Principle

Prefer small, focused interfaces over large interfaces that force clients to depend on operations they do not use.

D: Dependency Inversion Principle

High-level behavior should depend on meaningful abstractions rather than directly depending on infrastructure details.

Important caution

SOLID should not be applied mechanically. Overusing it can create:

  • Too many interfaces
  • Excessive indirection
  • Tiny classes with little value
  • Speculative abstractions
  • Harder navigation

Use the principles only when they solve actual design problems.

Status

Plain Text
[-] SOLID principles: skipped
13

Overview

Security starts with treating external data and systems as untrusted.

Validate external input

Validate:

  • Type and structure
  • Allowed values
  • Minimum and maximum ranges
  • String and collection lengths
  • File size and format
  • Business constraints

Prefer allowlists:

Python
SUPPORTED_FILE_TYPES = {
".csv",
".json",
".txt",
}

Accept only formats the application supports.

Avoid executing external input

Never use eval() on external input:

Python
# Unsafe
result = eval(user_input)

Use appropriate parsers such as json.loads().

Protect secrets

Do not place credentials in source code:

Python
import os

def load_api_key() -> str:
api_key = os.getenv("ORDER_API_KEY")

  • if not api_key:
  • raise RuntimeError(
  • "ORDER_API_KEY is not configured."
  • )

return api_key

Also keep secrets out of:

  • Logs
  • Test fixtures
  • Exceptions
  • Example configuration
  • Version control
  • Use parameterized SQL

Never build SQL commands using raw external strings:

Python
cursor.execute(
"SELECT * FROM customers WHERE email = ?",
(email_address,),
)

The placeholder syntax depends on the database library.

Set timeouts

External requests must have time limits:

Python
response = client.get(
endpoint,
timeout=10,
)
Retry carefully

Retry only:

  • Temporary failures
  • Safe or idempotent operations
  • With a maximum attempt count
  • With appropriate delay
  • Within an overall time budget

Retries can duplicate side effects such as payments unless the operation supports safe duplicate handling.

Use transactions

When multiple updates form one business operation, define how they succeed or fail together.

Examples include:

  • Saving an order
  • Reserving inventory
  • Recording payment
  • Fail safely

A dependency failure should not make security decisions more permissive:

Python
def has_permission(user) -> bool:
try:
return load_permissions(user)
except PermissionServiceError:
return False
Separate user errors from technical diagnostics

Users need safe, understandable messages. Developers need detailed logs.

Do not expose:

  • Tracebacks
  • Credentials
  • Internal paths
  • Database structure
  • Private host information
  • Use safe tools for sensitive operations

Examples:

  • secrets for unpredictable tokens
  • Maintained password-hashing libraries for passwords
  • Context managers for cleanup
  • Decimal when exact financial calculations are required
  • Managed secret stores where applicable
  • Test failure paths

Test:

  • Timeouts
  • Missing configuration
  • Invalid external responses
  • Transaction failures
  • Duplicate requests
  • Retry limits
  • Cleanup after failure
  • Safe fallback behavior
  • Main takeaway

Treat external data as untrusted, keep secrets out of code and logs, make failures explicit, and design important operations with their failure paths in mind.

14

Overview

Performance work should be based on evidence, not assumptions.

Recommended sequence
Make the code correct.
Make it clear.
Define a measurable target.
Measure the current performance.
Profile the application.
Optimize the actual bottleneck.
Measure again.
Verify correctness.
Define measurable requirements

Examples:

Plain Text
Process 100,000 orders in under two seconds.
Plain Text
Complete 95% of API requests within 300 milliseconds.
Plain Text
Use less than 500 MB of peak memory.
Measure time

Use timeit for small operations and perf_counter() for application sections:

Python
from time import perf_counter

start_time = perf_counter()

result = process_orders(orders)

elapsed = perf_counter() - start_time
Profile before optimizing

Use cProfile:

Shell
python -m cProfile -s cumulative app.py

Look for:

  • Frequently called functions
  • Expensive work inside loops
  • Repeated conversions
  • Unexpected I/O
  • Repeated database or network operations
  • Improve algorithms and data structures

Choose containers based on operations:

  • list: ordered values and indexing
  • set: uniqueness and membership checks
  • dict: key-based lookup
  • Generator: one-pass streaming

For repeated lookup by identifier:

Python
orders_by_id = {
order.order_id: order
for order in orders
}

This is often better than repeatedly searching a list.

Move invariant work outside loops

Python
settings = load_settings()

for order in orders:
process_order(order, settings)

Do not reload unchanged settings for every order.

Reduce external I/O

Database and network calls are often more expensive than Python calculations.

Consider:

  • Bulk loading
  • Batch inserts
  • Pagination
  • Connection reuse
  • Correct indexes
  • Avoiding N+1 queries
  • Selecting only required fields
  • Stream and batch large data

Process large files line by line:

Python
with file_path.open(encoding="utf-8") as data_file:
for line in data_file:
process_line(line)

Use batching when the external system supports bulk operations.

Cache only with a clear policy

Caching is appropriate when:

  • The operation is expensive
  • Inputs repeat
  • Results are stable
  • Memory is bounded
  • Staleness is acceptable or controlled

A cache adds complexity around expiration and invalidation.

Understand CPU-bound versus I/O-bound work

CPU-bound work may benefit from better algorithms, optimized libraries, or multiprocessing.
I/O-bound work may benefit from batching, connection reuse, or controlled concurrency.

Concurrency is not automatically faster. It introduces failure, cancellation, ordering, and resource-management concerns.

Preserve correctness

After every optimization:

  • Run all tests
  • Repeat the benchmark
  • Check memory use
  • Verify failure behavior
  • Review maintainability
  • Main takeaway

First make the code correct and clear. Then measure realistic workloads, optimize the real bottleneck, and verify the improvement without sacrificing correctness.

Final Combined Checklist

Design and organization

Plain Text
[ ] Is repeated business knowledge centralized?
[ ] Are abstractions meaningful and stable?
[ ] Does each module have a cohesive responsibility?
[ ] Is the entry point small?
[ ] Are dependencies clear and correctly directed?
Refactoring and objects
Plain Text
[ ] Are refactorings small and protected by tests?
[ ] Are unclear names and complex conditions improved?
[ ] Are classes used only when they add real value?
[ ] Do objects protect their valid state?
[ ] Is composition preferred over unnecessary inheritance?
Security and reliability
Plain Text
[ ] Is external input validated?
[ ] Are secrets kept outside source code and logs?
[ ] Are database queries parameterized?
[ ] Do external calls have timeouts?
[ ] Are retry and transaction policies explicit?
[ ] Are failure paths tested?
Performance
Plain Text
[ ] Is there a measurable performance problem?
[ ] Has the bottleneck been profiled?
[ ] Are appropriate algorithms and data structures used?
[ ] Are repeated database and network calls minimized?
[ ] Are streaming, batching, or caching justified?
[ ] Do tests still pass after optimization?
Updated Status

Clean code is not about applying every principle or pattern. It is about choosing structures that make the program clear, correct, secure, testable, maintainable, and appropriately efficient.

Apply it

One refactor, many principles

Use code smell recognition instead of memorizing isolated rules.

BEFORE · Muddled behavior
def process(order, db, notify=False):
    if order:
        if order["quantity"] > 0:
            total = order["price"] * order["quantity"]
            db.save(total)
            if notify:
                print("Saved")
            return total
    return "Invalid order"
AFTER · Clear responsibilities
def calculate_total(price: float, quantity: int) -> float:
    if price < 0 or quantity <= 0:
        raise ValueError("Invalid price or quantity")
    return price * quantity


def create_order(order, repository) -> float:
    total = calculate_total(order["price"], order["quantity"])
    repository.save(total)
    return total
What improved? Intent-revealing names · early validation · shallow control flow · predictable return type · separated calculation and storage · injectable dependency. Note: for a real application, validate the raw order's structure at its input boundary too.
A mnemonic you'll actually remember

Ask yourself: CLEAN

Five questions before you mark a task done.

Put it to work

Pre-commit code review

Tick these as you inspect a change. Progress stays in this browser.

0 / 10 checked
Retention beats repetition

Your five-minute daily routine

Use a real pull request or a function from your project as the learning material.

01 · ONE MINUTERecall

Close the notes. Say one principle and one warning sign from memory.

02 · TWO MINUTESRecognize

Find one example in code: nesting, hidden I/O, duplicated rules, weak tests.

03 · TWO MINUTESImprove

Make one small, test-protected change, or explain why no change is needed.

Revisit the topics over time

Suggested review pattern: day 1 (functions + control flow), day 2 (validation + errors), day 4 (architecture + tests), day 7 (typing + models), day 14 (DRY + OOP + refactoring), day 30 (security + performance). Practice recall before reopening the full notes.