Python clean code tip - use singledispatch instead of isinstance
Python clean code tip:
Use
singledispatch
instead ofisinstance
.👇
from dataclasses import dataclass from functools import singledispatch @dataclass class UserCanceledSubscription: username: str @dataclass class UserSubscribed: username: str # with if def process(event): if isinstance(event, UserSubscribed): print(f"Enable access to user {event.username}") elif isinstance(event, UserCanceledSubscription): print(f"Disable access to user {event.username}") # with singledispatch @singledispatch def process(event): pass @process.register(UserCanceledSubscription) def _(event): print(f"Disable access to user {event.username}") @process.register(UserSubscribed) def _(event): print(f"Enable access to user {event.username}") events = [ UserSubscribed(username="johndoe"), UserCanceledSubscription(username="johndoe"), ] for event in events: process(event)
Python clean code tip - always use paginated queries
Python clean code tip:
Always use paginated queriesl
Use the "last evaluated record" approach to paginate instead of offset. This way you limit database load per single query.
👇
from sqlalchemy.orm import Session, sessionmaker, declarative_base from sqlalchemy import create_engine, Column, Integer, String SQLALCHEMY_DATABASE_URL = "sqlite:///./example.db" engine = create_engine( SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} ) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) email = Column(String, unique=True, index=True) def __repr__(self): return f"User(id={self.id}, email={self.email})" class SQLiteUserRepository: BATCH_SIZE = 1000 def __init__(self, database_session): self._database_session = database_session def add(self, user): with self._database_session.begin(): self._database_session.add(user) def list_all(self): last_fetched_id = -1 users = [] while True: users_batch = ( self._database_session.query(User) .filter(User.id > last_fetched_id) .order_by(User.id) .limit(self.BATCH_SIZE) .all() ) users.extend(users_batch) if not users_batch: break last_fetched_id = users_batch[-1].id return users Base.metadata.create_all(bind=engine) database_session = sessionmaker(engine)() repository = SQLiteUserRepository(database_session) repository.add(User(email="[email protected]")) repository.add(User(email="[email protected]")) print(repository.list_all()) # [User(id=1, [email protected]), User(id=2, [email protected])]
Python - inline if statement for print
Python tip:
You can write an if-else inside the
def is_adult(age): print("Adult" if age > 18 else "Child") is_adult(17) # -> Child is_adult(32) # -> Adult age = 10 print("Adult" if age > 18) # -> SyntaxError: expected 'else' after 'if' expression
How to check if all elements in a Python iterable are True
Python tip:
You can use all() to check if all elements in an iterable are
True
:def class_completed(exams): if all(exams): print("You've passed all the exams.") else: print("You didn't pass all of the exams.") student_that_passed = [True, True, True, True] student_that_failed = [True, True, False, True] class_completed(student_that_passed) # -> You've passed all the exams. class_completed(student_that_failed) # -> You didn't pass all of the exams.
How to check if any element in a Python iterable is True
Python tip:
You can use any() to check if any element in iterable is
True
:def allow_access(role): allow_access_for_roles = [role == "superuser", role == "owner", role == "supervisor"] if any(allow_access_for_roles): print("Access allowed") else: print("Access denied") allow_access("superuser") # -> Access allowed allow_access("member") # -> Access denied
Python - Truthy and Falsy Values
Python tip:
In Python, individual values can evaluate as either True or False. Values that evaluate to
True
are "Truthy", and values that evaluate toFalse
are "Falsy".By default, an object is considered Truthy, unless its
__bool__()
method returnsFalse
or__len__()
returns0
.👇
empty_list = [] empty_touple = () empty_dict = {} empty_set = set() empty_string = "" empty_range = range(0) print(bool(empty_list)) # False print(bool(empty_touple)) # False print(bool(empty_dict)) # False print(bool(empty_set)) # False print(bool(empty_string)) # False print(bool(empty_range)) # False print(bool(0)) # False print(bool(0.0)) # False print(bool(0j)) # False print(bool(None)) # False print((bool(1))) # True print((bool(" "))) # True
Python - sum of all counts in collections.Counter
Python tip:
You can compute the sum of all the counts from a
Counter
with total():from collections import Counter pencil_stock = Counter({"Red": 17, "Blue": 5, "Green": 9}) print(pencil_stock.total()) # -> 31
Python - find the most common elements in an iterable
Python tip:
To find the most common elements in an iterable, you can use Counter.most_common.
The counter returns a list of tuples, where each tuple contains the element and the element count.
from collections import Counter most_common_numbers = Counter([1, 5, 6, 5, 3, 1, 2, 5]).most_common(2) print(most_common_numbers) # -> [(5, 3), (1, 2)] most_common_letters = Counter("abcbadfbcb").most_common(3) print(most_common_letters) # -> [('b', 4), ('a', 2), ('c', 2)]
Python - Else conditional statement inside a for loop
Python tip:
If your for loop includes a conditional statement, you can use an else statement in your loop. The else clause is executed if the loop is not terminated with a break statement.
def contains_number_bigger_than_1000(numbers_list): for number in numbers_list: if number > 1000: print("list CONTAINS a number bigger than 1000") break else: print("list DOES NOT contain a number bigger than 1000") contains_number_bigger_than_1000([1, 200, 50]) # -> list DOES NOT contain a number bigger than 1000 contains_number_bigger_than_1000([1, 200, 5000]) # -> list CONTAINS a number bigger than 1000
Sort your Python module imports automatically with isort
Python tip:
Instead of trying to keep your imports in order by hand, you can use the isort library.
# install isort $ pip install isort # sort a specific file $ isort myfile.py # sort all Python files, recursively $ isort . # see the differences without applying them $ isort myfile.py --diff # confirm changes before applying them $ isort . --interactive # check if the imports are sorted properly $ isort . --check
Python - enforcing type hints with mypy
Python tip:
To enforce type hints, you can use mypy, a static type checker for Python.
For example, if we use the
List[float]
type hint for the function parameter and then provide a dictionary in a function call:def all_miles_runned(runs: List[float]) -> float: return sum(runs) print(all_miles_runned({1653476791: 5, 1653908791: 7.2, 1654081591: 8.3})) """ with mypy $ pip install mypy $ python -m mypy miles_runned.py miles_runned.py:11: error: Argument 1 to "all_miles_runned" has incompatible type "Dict[int, float]"; expected "List[float]" """
Generate a UUID in Python
Python tip:
With Python's uuid module, you can generate a Universally Unique Identifier. While the module provides four different UUID versions (1, 3, 4, and 5), you'll probably want to use version 4,
uuid4
, since it produces a random UUID:import uuid print(uuid.uuid4()) # -> 4388b8ef-caa8-43b5-b7e6-4ef9ab89fe51
Open a web browser with Python's webbrowser module
Python tip:
Python's standard library includes a webbrowser module that allows you to open a web-based document in a browser.
👇
import webbrowser import tempfile html_page=""" <html> <head> <title>Testdriven.io</title> </head> <body> I'm going to open in a browser. </body> </html> """ with tempfile.NamedTemporaryFile('w', delete=False, suffix='.html') as f: url = 'file://' + f.name f.write(html_page) webbrowser.open(url)
Python HTMLCalendar - formatyearpage()
Python tip:
Did you know you can generate a complete HTML page with a yearly calendar using Python?
import calendar import tempfile import webbrowser cal = calendar.HTMLCalendar() html_calendar = cal.formatyearpage(2022) with tempfile.NamedTemporaryFile('w', delete=False, suffix='.html') as f: url = 'file://' + f.name f.write(html_calendar.decode()) webbrowser.open(url)
Python TextCalendar - formatyear() and pryear()
Python tip:
You can output a calendar year as a multi-line string using Python's
calendar
module.You need to provide the year, and you can impact how it looks with optional parameters.
You can use formatyear to get the string or pryear to directly print the output.
import calendar cal = calendar.TextCalendar() year = cal.formatyear(2022, m=4) print(year) # or cal.pryear(2022, m=4)
Python TextCalendar - formatmonth() and prmonth()
Python tip:
Python's
calendar
module lets you output a calendar month as a multi-line string. The required parameters are the year and the month.You can use formatmonth to generate the string or prmonth to directly print it.
formatmonth
:import calendar cal = calendar.TextCalendar() month = cal.formatmonth(2022, 6) print(month) """ Mo Tu We Th Fr Sa Su 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 """
prmonth
:import calendar cal = calendar.TextCalendar() cal.prmonth(2022, 6) """ June 2022 Mo Tu We Th Fr Sa Su 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 """
Python - yeardatescalendar()
Python tip:
You can get a list of all months, containing all the dates for a year with the yeardatescalendar method.
You need to provide the date and width as arguments.
The return looks like this:
[list of months[month[week[datetime object]]]]
import calendar cal = calendar.Calendar() year = cal.yeardatescalendar(2022, 6) for month in year: print(month) """ Results: [ [ [ datetime.date(2021, 12, 27), datetime.date(2021, 12, 28), datetime.date(2021, 12, 29), datetime.date(2021, 12, 30), datetime.date(2021, 12, 31), datetime.date(2022, 1, 1), datetime.date(2022, 1, 2), ], [ datetime.date(2022, 1, 3), datetime.date(2022, 1, 4), datetime.date(2022, 1, 5), datetime.date(2022, 1, 6), datetime.date(2022, 1, 7), datetime.date(2022, 1, 8), datetime.date(2022, 1, 9), ], ... ], ] """
Python - monthdays2calendar()
Python tip:
To get a list of the weeks in a certain month, INCLUDING weekday numbers, you can use the monthdays2calendar method.
You need to provide the date and month as arguments.
A list of lists, each containing seven tuples of day numbers and weekday numbers, is returned.
import calendar cal = calendar.Calendar() weeks = cal.monthdays2calendar(2022, 7) print(weeks) """ Results: [ [(0, 0), (0, 1), (0, 2), (0, 3), (1, 4), (2, 5), (3, 6)], [(4, 0), (5, 1), (6, 2), (7, 3), (8, 4), (9, 5), (10, 6)], [(11, 0), (12, 1), (13, 2), (14, 3), (15, 4), (16, 5), (17, 6)], [(18, 0), (19, 1), (20, 2), (21, 3), (22, 4), (23, 5), (24, 6)], [(25, 0), (26, 1), (27, 2), (28, 3), (29, 4), (30, 5), (31, 6)], ] """
Python - monthdatescalendar()
Python tip:
To get a list of weeks in a certain month, you can use the monthdatescalendar method.
You need to provide the date and month as arguments.
A list of lists, each containing 7
datetime.date
objects is returned.import calendar cal = calendar.Calendar() weeks = cal.monthdatescalendar(2022, 2) print(weeks) """ Results: [ [ datetime.date(2022, 1, 31), datetime.date(2022, 2, 1), datetime.date(2022, 2, 2), datetime.date(2022, 2, 3), datetime.date(2022, 2, 4), datetime.date(2022, 2, 5), datetime.date(2022, 2, 6), ], [ datetime.date(2022, 2, 7), datetime.date(2022, 2, 8), datetime.date(2022, 2, 9), datetime.date(2022, 2, 10), datetime.date(2022, 2, 11), datetime.date(2022, 2, 12), datetime.date(2022, 2, 13), ], [ datetime.date(2022, 2, 14), datetime.date(2022, 2, 15), datetime.date(2022, 2, 16), datetime.date(2022, 2, 17), datetime.date(2022, 2, 18), datetime.date(2022, 2, 19), datetime.date(2022, 2, 20), ], [ datetime.date(2022, 2, 21), datetime.date(2022, 2, 22), datetime.date(2022, 2, 23), datetime.date(2022, 2, 24), datetime.date(2022, 2, 25), datetime.date(2022, 2, 26), datetime.date(2022, 2, 27), ], [ datetime.date(2022, 2, 28), datetime.date(2022, 3, 1), datetime.date(2022, 3, 2), datetime.date(2022, 3, 3), datetime.date(2022, 3, 4), datetime.date(2022, 3, 5), datetime.date(2022, 3, 6), ], ] """
Python - itermonthdays4()
Python tip:
To get complete dates (including a day in a week) for a certain month, you can use the itermonthdays4 method.
Returned days will be tuples, consisting of a year, a month, a day of the month, and a week day number.
import calendar cal = calendar.Calendar() days = cal.itermonthdays4(2022, 2) for day in days: print(day) """ Results: (2022, 1, 31, 0) (2022, 2, 1, 1) (2022, 2, 2, 2) (2022, 2, 3, 3) ... (2022, 3, 4, 4) (2022, 3, 5, 5) (2022, 3, 6, 6) """
Python - itermonthdays2()
Python tip:
To get a date and a day in a week for a specific month, you can use the itermonthdays2 method.
Returned days will be tuples, consisting of a day of the month number and a week day number.
Day numbers outside this month are zero.
import calendar cal = calendar.Calendar() days = cal.itermonthdays2(2022, 7) for day in days: print(day) """ Results: (0, 0) (0, 1) (0, 2) (0, 3) (1, 4) (2, 5) (3, 6) ... (29, 4) (30, 5) (31, 6) """
Python - itermonthdates()
Python tip:
You can get an iterator for a certain month by using the itermonthdates method. You need to provide a year and a month as parameters.
The iterator returns all days before the start / after the end of the month that are required to get a full week.
import calendar cal = calendar.Calendar() for day in cal.itermonthdates(2022, 7): print(day) """ Results: 2022-06-27 2022-06-28 2022-06-29 2022-06-30 2022-07-01 2022-07-02 ... 2022-07-30 2022-07-31 """
Python Testing - Mocking different responses for consecutive calls
Python testing tip:
You can specify different responses for consecutive calls to your
MagicMock
.It's useful when you want to mock a paginated response.
👇
from unittest.mock import MagicMock def test_all_cars_are_fetched(): get_cars_mock = MagicMock() get_cars_mock.side_effect = [ ["Audi A3", "Renault Megane"], ["Nissan Micra", "Seat Arona"] ] print(get_cars_mock()) # ['Audi A3', 'Renault Megane'] print(get_cars_mock()) # ['Nissan Micra', 'Seat Arona']
Type Hints - How to Use typing.cast() in Python
Python tip:
You can use
cast()
to signal to a type checker that the value has a designated type.https://docs.python.org/3/library/typing.html#typing.cast
👇
from dataclasses import dataclass from enum import Enum from typing import cast class BoatStatus(int, Enum): RESERVED = 1 FREE = 2 @dataclass class Boat: status: BoatStatus Boat(status=2) # example.py:16: error: Argument "status" to "Boat" has incompatible type "int"; expected "BoatStatus" # Found 1 error in 1 file (checked 1 source file) Boat(status=cast(BoatStatus, 2))
SQLAlchemy with_for_update
SQLAlchemy tip:
You can use
with_for_update()
to use SELECT FOR UPDATE. This will prevent changes in selected rows before you commit your work to the database.https://docs.sqlalchemy.org/en/14/orm/query.html#sqlalchemy.orm.Query.with_for_update
👇
user = session.query(User).filter(User.email == email).with_for_update().first() user.is_active = True session.add(user) session.commit()
Python - set isdisjoint()
Python tip:
You can use
.isdisjoint()
to check whether the intersection of two sets is empty -- i.e., there are not elements that are in the first and second sets.👇
winners = {"Carl", "Dan"} players = {"Daisy", "John", "Bob"} print(players.isdisjoint(winners)) # => True
Python - set symmetric_difference()
Python tip:
You can use
.symmetric_difference()
to get a new set containing elements that are either in the first or second set but not in both.For example:
winners = {"John", "Marry"} players = {"Daisy", "John", "Bob"} print(players.symmetric_difference(winners)) # => {'Bob', 'Daisy', 'Marry'}
Python - set issubset()
Python tip:
You can use
.issubset()
to check whether the second set contains the first one.👇
winners = {"John", "Marry"} players = {"Daisy", "John", "Bob", "Marry"} print(winners.issubset(players)) # => True
How to get the difference between two sets in Python
Python tip:
You can use
.difference()
to get a new set that contains unique elements that are in the first set but not in the second one.For example:
winners = {"John", "Marry"} players = {"Daisy", "John", "Bob", "Marry"} print(players.difference(winners)) # => {'Bob', 'Daisy'}
Python Set Intersection
Python tip:
You can use
.intersection()
to get a new set containing unique elements that are present inside two sets.👇
winners = {"John", "Marry"} players = {"Daisy", "John", "Bob", "Marry"} print(winners.intersection(players)) # => {'John', 'Marry'}
Union multiple sets in Python
Python tip:
You can use
.union()
to create a new set containing unique elements that are either in the first set, the second set, or in both of them.👇
winners = {"John", "Marry"} players = {"Daisy", "John", "Bob", "Marry"} print(winners.union(players)) # => {'John', 'Marry', 'Bob', 'Daisy'}
Using a Python dictionary as a switch statement
Python tip:
You can use a dictionary to implement switch-like behavior.
For example:
class ProductionConfig: ELASTICSEARCH_URL = "https://elasticsearch.example.com" class DevelopmentConfig: ELASTICSEARCH_URL = "https://development-elasticsearch.example.com" class TestConfig: ELASTICSEARCH_URL = "http://test-in-docker:9200" CONFIGS = { "production": ProductionConfig, "development": DevelopmentConfig, "test": TestConfig, } def load_config(environment): return CONFIGS.get(environment, ProductionConfig) print(load_config("production")) # <class '__main__.ProductionConfig'> print(load_config("test")) # <class '__main__.TestConfig'> print(load_config("unknown")) # <class '__main__.ProductionConfig'>
Passing a dictionary as keyword arguments to a function in Python
Python tip:
You can use
**
to unpack a dictionary as keyword arguments (kwargs) for a function.👇
user = {"name": "Jan", "surname": "Giacomelli"} def print_full_name(name, surname): print(f"{name} {surname}") print_full_name(**user) # => Jan Giacomelli
Remove duplicates from a Python list while preserving order
Python tip:
You can use a dictionary to remove duplicates from a list while preserving the order of elements:
users = ["Jan", "Mike", "Marry", "Mike"] print(list({user: user for user in users})) # => ['Jan', 'Mike', 'Marry']
A dictionary preserves the insertion order.
How do I join dictionaries together in Python?
Python tip:
You can join two dictionaries using
**
or|
(for Python >= 3.9, works for all subclasses).If there are any duplicate keys, the second (rightmost) key-value pair is used.
user = {"name": "Jan", "surname": "Giacomelli"} address = {"address1": "Best street 42", "city": "Best city"} user_with_city = {**user, **address} print(user_with_city) # {'name': 'Jan', 'surname': 'Giacomelli', 'address1': 'Best street 42', 'city': 'Best city'} user_with_city = user | address print(user_with_city) # {'name': 'Jan', 'surname': 'Giacomelli', 'address1': 'Best street 42', 'city': 'Best city'} user_with_city = {"address": "Best street"} | {"address": "Almost best street"} print(user_with_city) # {'address': 'Almost best street'}
Prevent KeyError when working with dictionaries in Python
Python tip:
You can use
.get()
to avoid a KeyError when accessing non-existing keys inside a dictionary:user = {"name": "Jan", "surname": "Giacomelli"} print(user.get("address", "Best street 42")) # => Best street 42 print(user["address"]) # => KeyError: 'address'
If you don't provide a default value,
.get()
will returnNone
if the key doesn't exist.If you're just trying to see if the key exists, it's better to use the
in
operator like so due to performance reasons:# good if "address" in user: print("yay") else: print("nay") # bad if user.get("address"): print("yay") else: print("nay")
Unpacking a list in Python
Python tip:
You can unpack list elements to variables. You can also ignore some of the elements.
👇
tournament_results = ["Jan", "Mike", "Marry", "Bob"] first_player, *_, last_player = tournament_results print(first_player, last_player) # => Jan Bob *_, last_player = tournament_results print(last_player) # => Bob first_player, *_ = tournament_results print(first_player) # => Jan first_player, second_player, third_player, fourth_player = tournament_results print(first_player, second_player, third_player, fourth_player) # => Jan Mike Marry Bob
Python - Iterate over multiple lists simultaneously with zip
Python tip:
You can use
zip
to iterate through multiple lists of equal length in a single loop.👇
users = ["Jan", "Mike", "Marry", "Mike"] user_visits = [10, 31, 10, 1] for user, visits in zip(users, user_visits): print(f"{user}: {visits}") # Jan: 10 # Mike: 31 # Marry: 10 # Mike: 1
Count the number of occurrences of an element in a list in Python
Python tip:
You can count occurrences of an element in a list with
.count()
.For example:
users = ["Jan", "Mike", "Marry", "Mike"] print(users.count("Mike")) # => 2
How do I concatenate two lists in Python?
Python tip:
You can use
+
to join two lists into a new list.a = [10, 2] b = [6, 3] print(a + b) # => [10, 2, 6, 3]
Python - create a list from a list repeated N times
Python tip:
You can create a new list with elements from the first list that are repeated as many times as you want by multiplying.
Fo example:
users = ["johndoe", "marry", "bob"] print(3 * users) # => ['johndoe', 'marry', 'bob', 'johndoe', 'marry', 'bob', 'johndoe', 'marry', 'bob']
Execute raw SQL queries in SQLAlchemy
Python SQLAlchemy tip:
You can use raw queries while still using SQLAlchemy models.
For example
user = session.query(Course).from_statement( text("""SELECT * FROM courses where title=:title""") ).params(title="Scalable FastAPI Applications on AWS").all()
Python - sep parameter in print()
Python tip:
You can pass as many values to print to the
print()
function as you want. You can also specify a custom separator.print("123", "456", "789") # => 123 456 789 print("123", "456", "789", sep="-") # => 123-456-789
How to flush output of print in Python?
Python tip:
You can set
flush=True
for theprint()
function to avoid buffering the output data and forcibly flush it:print("I'm awesome", flush=True)
Python - find the last occurrence of an item in a list with rindex()
Python tip:
You can use
.rindex()
to find the highest index in a string where a substring is found.👇
print("2021 was awesome. 2022 is going to be even more awesome.".rindex("awesome")) # => 48
Python - string ljust() method
Python tip:
You can use
.ljust()
to create a left-justified string of given width.string.ljust(width, fillchar)
Padding is a space, " ", by default.
print("Mike".ljust(10, "*")) # => Mike******
Python - string center() method
Python tip:
You can use
.center()
to create a centered string of given width.string.center(width, fillchar)
Padding on each side is a space, " ", by default.
print("Mike".center(10, "*")) # => ***Mike***
Python - lower() vs. casefold() for string matching and converting to lowercase
Python tip:
Use
.casfolde()
instead of.lower()
when you want to perform caseless operations when working with Unicode strings (for ASCII only strings they work the same) -- e.g., check if two strings are equal.# In German ß == ss print("straße".lower() == "strasse") # False print("straße".casefold() == "strasse") # True
Python - remove a prefix from a string
Python tip (>=3.9):
You can use
.removeprefix()
to remove the prefix from a string.For example, to remove a filename prefix:
invoice_filenames = ("INV_123.pdf", "INV_234.pdf", "INV_345.pdf") for invoice_filename in invoice_filenames: print(invoice_filename.removeprefix("INV_")) # 123.pdf # 234.pdf # 345.pdf
Python - remove a suffix from a string
Python tip (>=3.9):
You can remove the suffix of a string with
.removesuffix()
.For example, to remove the file type from a filename:
import pathlib filename = "cv.pdf" file_type_suffix = pathlib.Path(filename).suffix print(filename.removesuffix(file_type_suffix)) # => cv
Contract Testing in Python
Python clean code tip:
Use contract testing when you want to verify the same behavior for different implementations.
Example:
import json import pathlib from dataclasses import dataclass import pytest @dataclass class User: username: str class InMemoryUserRepository: def __init__(self): self._users = [] def add(self, user): self._users.append(user) def get_by_username(self, username): return next(user for user in self._users if user.username == username) class JSONUserRepository: def __init__(self, file_path): self._users = json.load(pathlib.Path(file_path).open()) def add(self, user): self._users.append(user) def get_by_username(self, username): return next(user for user in self._users if user.username == username) class UserRepositoryContract: @pytest.fixture def repository(self): raise NotImplementedError('Not Implemented Yet') @pytest.fixture def username(self): return 'johndoe' @pytest.fixture def user(self, username): return User(username=username) def test_added_user_is_retrieved_by_username(self, username, user, repository): repository.add(user) assert repository.get_by_username(user.username).username == username class TestInMemoryUserRepository(UserRepositoryContract): @pytest.fixture def repository(self): return InMemoryUserRepository() class TestInJSONUserRepository(UserRepositoryContract): @pytest.fixture def repository(self, tmp_path): users_file = tmp_path/"user.json" users_file.write_text(json.dumps([])) return JSONUserRepository(users_file)
Simplify Testing with Dependency Injection
Python clean code tip:
Use dependency injection to simplify testing
Example:
from dataclasses import dataclass from fastapi import FastAPI @dataclass class User: username: str class StartUserOnboarding: def __init__(self, user_repository): self._user_repository = user_repository def execute(self, username): user = User(username=username) self._user_repository.add(user) class InMemoryUserRepository: def __init__(self): self._users = [] def add(self, user): self._users.append(user) def get_by_username(self, username): return next(user for user in self._users if user.username == username) class SQLiteUserRepository: def __init__(self, config): self._config = config def add(self, user): print(f"Running some SQL statements for insert DATABASE_PATH") def get_by_username(self, username): print(f"Running some SQL statements for fetch from {self._config.DATABASE_PATH}") def test_user_is_added_to_repository(): username = "[email protected]" repository = InMemoryUserRepository() use_case = StartUserOnboarding(user_repository=repository) use_case.execute(username) assert repository.get_by_username(username).username class ApplicationConfig: DATABASE_PATH = "db" app = FastAPI() @app.post("/users/start-onboarding", status_code=202) async def start_user_onboarding(username: str): StartUserOnboarding(SQLiteUserRepository(ApplicationConfig())).execute(username) return "OK"
Python - use enums to group related constants
Python clean code tip:
Use enums to group related constants.
Why?
- Autocomplete
- Static type checking
Example:
from dataclasses import dataclass from enum import Enum # bad ORDER_PLACED = "PLACED" ORDER_CANCELED = "CANCELED" ORDER_FULFILLED = "FULFILLED" @dataclass class Order: status: str order = Order(ORDER_PLACED) print(order) # better class OrderStatus(str, Enum): PLACED = "PLACED" CANCELED = "CANCELED" FULFILLED = "FULFILLED" @dataclass class Order: status: OrderStatus order = Order(OrderStatus.PLACED) print(order)
Interfaces in Python with Protocol Classes
Python clean code tip:
Use
Protocol
to define the interface required by your function/method instead of using real objects. This way your function/method defines what it needs.from typing import Protocol class ApplicationConfig: DEBUG = False SECRET_KEY = "secret-key" EMAIL_API_KEY = "api-key" # bad def send_email(config: ApplicationConfig): print(f"Send email using API key: {config.EMAIL_API_KEY}") # better class EmailConfig(Protocol): EMAIL_API_KEY: str def send_email_(config: EmailConfig): print(f"Send email using API key: {config.EMAIL_API_KEY}")
Python - Property-based Testing with Hypothesis
Python testing tip:
Rather than having to write different test cases for every argument you want to test, property-based testing generates a wide-range of random test data that's dependent on previous tests runs.
Use Hypothesis for this:
def increment(num: int) -> int: return num + 1 # regular test import pytest @pytest.mark.parametrize( 'number, result', [ (-2, -1), (0, 1), (3, 4), (101234, 101235), ] ) def test_increment(number, result): assert increment(number) == result # property-based test from hypothesis import given import hypothesis.strategies as st @given(st.integers()) def test_add_one(num): assert increment(num) == num - 1
Python - mock.create_autospec()
Python tip:
Use
mock.create_autospec()
to create a mock object with methods that have the same interface as the ones inside the original object.Example:
from unittest import mock import requests from requests import Response def get_my_ip(): response = requests.get( 'http://ipinfo.io/json' ) return response.json()['ip'] def test_get_my_ip(monkeypatch): my_ip = '123.123.123.123' response = mock.create_autospec(Response) response.json.return_value = {'ip': my_ip} monkeypatch.setattr( requests, 'get', lambda *args, **kwargs: response ) assert get_my_ip() == my_ip
Arrange-Act-Assert - testing pattern
Python clean test tip:
Structure your tests in an Arrange-Act-Assert way:
- Arrange - set-up logic
- Act - invokes the system you're about to test
- Assert - verifies that the action of the system under test behaves as expected
Example:
from dataclasses import dataclass @dataclass class User: first_name: str last_name: str def full_name(self): return f"{self.first_name} {self.last_name}" def test_full_name_consists_of_first_name_and_last_name(): # arrange first_name = "John" last_name = "Doe" user = User(first_name=first_name, last_name=last_name) # act full_name = user.full_name() # assert assert full_name == "John Doe"
Hide irrelevant test data
Python clean test tip:
You should hide irrelevant data for the test.
Such information just increases the cognitive mental load, resulting in bloated tests.
Example:
import uuid from dataclasses import dataclass from enum import Enum from uuid import UUID import pytest class ProductCategory(str, Enum): BOOK = "BOOK" ELECTRONIC = "ELECTRONIC" @dataclass class Product: id: UUID price: int name: str category: ProductCategory class ShoppingCart: def __init__(self): self._products = [] def add(self, product): self._products.append(product) def calculate_total_price(self): return sum(product.price for product in self._products) # BAD - category, id, and name are irrelevant for this test def test_given_products_with_total_price_50_when_calculate_total_price_then_total_price_is_50_(): shopping_cart = ShoppingCart() shopping_cart.add(Product(uuid.uuid4(), 10, "Mobile phone case", ProductCategory.ELECTRONIC)) shopping_cart.add(Product(uuid.uuid4(), 20, "Never enough", ProductCategory.BOOK)) shopping_cart.add(Product(uuid.uuid4(), 20, "Mobile phone charger", ProductCategory.ELECTRONIC)) assert shopping_cart.calculate_total_price() == 50 # GOOD @pytest.fixture def product_with_price(): def _product_with_price(price): return Product(uuid.uuid4(), price, "Mobile phone case", ProductCategory.ELECTRONIC) return _product_with_price def test_given_products_with_total_price_50_when_calculate_total_price_then_total_price_is_50(product_with_price): shopping_cart = ShoppingCart() shopping_cart.add(product_with_price(10)) shopping_cart.add(product_with_price(20)) shopping_cart.add(product_with_price(20)) assert shopping_cart.calculate_total_price() == 50
Tests should use meaningful data
Python clean test tip:
Your tests should use meaningful data in order to provide examples of how to use your code.
Examples:
from dataclasses import dataclass @dataclass class Car: manufacture: str model: str vin_number: str top_speed: int class InMemoryCarRepository: def __init__(self): self._cars = [] def add(self, car): self._cars.append(car) def get_by_vin_number(self, vin_number): return next(car for car in self._cars if car.vin_number == vin_number) # BAD - non-existing manufacture and model, VIN number not matching manufacture and model, impossible to reach top speed def test_added_car_can_be_retrieved_by_vin_number_(): car = Car(manufacture="AAAA", model="BBB+", vin_number="2FTJW36M6LCA90573", top_speed=1600) repository = InMemoryCarRepository() repository.add(car) assert car == repository.get_by_vin_number(car.vin_number) # GOOD def test_added_car_can_be_retrieved_by_vin_number(): car = Car(manufacture="Jeep", model="Wrangler", vin_number="1J4FA29P4YP728937", top_speed=160) repository = InMemoryCarRepository() repository.add(car) assert car == repository.get_by_vin_number(car.vin_number)
What should tests cover?
Python clean test tip:
For the most part, the tests you write should cover:
- all happy paths
- edge/corner/boundary cases
- negative test cases
- security and illegal issues
👇
import uuid from dataclasses import dataclass from typing import Optional @dataclass class User: username: str class InMemoryUserRepository: def __init__(self): self._users = [] def add(self, user: User) -> None: self._users.append(user) def search(self, query: Optional[str] = None) -> list[User]: if query is None: return self._users else: return [ user for user in self._users if query in user.username ] # happy path def test_search_users_without_query_lists_all_users(): user1 = User(username="[email protected]") user2 = User(username="[email protected]") repository = InMemoryUserRepository() repository.add(user1) repository.add(user2) assert repository.search() == [user1, user2] # happy path def test_search_users_with_email_part_lists_all_matching_users(): user1 = User(username="[email protected]") user2 = User(username="[email protected]") user3 = User(username="[email protected]") repository = InMemoryUserRepository() repository.add(user1) repository.add(user2) repository.add(user3) assert repository.search("doe") == [user1, user3] # edge test case def test_search_users_with_empty_query_lists_all_users(): user1 = User(username="[email protected]") user2 = User(username="[email protected]") repository = InMemoryUserRepository() repository.add(user1) repository.add(user2) assert repository.search("") == [user1, user2] # negative test case def test_search_users_with_random_query_lists_zero_users(): user1 = User(username="[email protected]") repository = InMemoryUserRepository() repository.add(user1) assert repository.search(str(uuid.uuid4())) == [] # security test def test_search_users_with_sql_injection_has_no_effect(): user1 = User(username="[email protected]") repository = InMemoryUserRepository() repository.add(user1) repository.search("DELETE FROM USERS;") assert repository.search() == [user1]
Tests should validate themselves regardless of whether the test execution passes or fails
Python clean test tip:
A test should validate itself whether the test execution is passed or failed.
The self-validating test can avoid the need to do an evaluation manually by us.
Example:
from dataclasses import dataclass @dataclass class User: first_name: str last_name: str def fullname(self): return f"{self.first_name} {self.last_name}" # BAD def test_full_name_consists_of_first_name_and_last_name_manual(): first_name = "John" last_name = "Doe" user = User(first_name=first_name, last_name=last_name) print(user.fullname()) assert input("Is result correct? (Y/n)") == "Y" # GOOD def test_full_name_consists_of_first_name_and_last_name(): first_name = "John" last_name = "Doe" full_name = "John Doe" user = User(first_name=first_name, last_name=last_name) assert user.fullname() == full_name
Tests should be independent
Python clean test tip:
A test should not depend on the state of any other tests or external services.
👇
from dataclasses import dataclass import pytest @dataclass class User: username: str class InMemoryUserRepository: def __init__(self): self._users = [] def add(self, user: User) -> None: self._users.append(user) def get_by_username(self, username: str) -> User: return next( user for user in self._users if user.username == username ) # BAD - depends on persistence layer having user record at test time def test_get_by_username(): user = User(username="[email protected]") repository = InMemoryUserRepository() assert repository.get_by_username(user.username) == user # BAD - test_user_is_fetched_by_username will succeed only when running after test_added_user @pytest.fixture(scope="module") def repository(): return InMemoryUserRepository() def test_added_user(repository): user = User(username="[email protected]") assert repository.add(user) is None def test_user_is_fetched_by_username(repository): user = User(username="[email protected]") assert repository.get_by_username(user.username) == user # GOOD - makes sure it has all the needed data def test_added_user_is_fetched_by_username(): user = User(username="[email protected]") repository = InMemoryUserRepository() repository.add(user) assert repository.get_by_username(user.username) == user
Tests should be repeatable and deterministic
Python clean test tip:
Your tests should be repeatable in any environment.
They should be deterministic, always result in the same tests succeeding.
Example:
import random LOTTO_COMBINATION_LENGTH = 5 MIN_LOTTO_NUMBER = 1 MAX_LOTTO_NUMBER = 42 def lotto_combination(): combination = [] while len(combination) < LOTTO_COMBINATION_LENGTH: number = random.randint(MIN_LOTTO_NUMBER, MAX_LOTTO_NUMBER) if number not in combination: combination.append(number) return combination # BAD def test_lotto_combination(): assert lotto_combination() == [10, 33, 5, 7, 2] # GOOD def test_all_numbers_are_between_min_max_range(): assert all(MIN_LOTTO_NUMBER <= number <= MAX_LOTTO_NUMBER for number in lotto_combination()) def test_length_of_lotto_combination_has_expected_number_of_elements(): assert len(lotto_combination()) == LOTTO_COMBINATION_LENGTH
Shorten your feedback loops by increasing the speed of your test suite
Python clean test tip:
Your tests should be fast. The faster the tests the faster the feedback loop.
Consider using mocks or test doubles when dealing with third-party APIs and other slow things.
Example:
import time def fetch_articles(): print("I'm fetching articles from slow API") time.sleep(10) return {"articles": [{"title": "Facebook is Meta now."}]} # BAD def test_fetch_articles_slow(): assert fetch_articles() == {"articles": [{"title": "Facebook is Meta now."}]} # GOOD def test_fetch_articles_fast(monkeypatch): monkeypatch.setattr(time, "sleep", lambda timeout: None) assert fetch_articles() == {"articles": [{"title": "Facebook is Meta now."}]}
Tests should be useful
Python clean test tip:
Tests should protect you against regressions. They shouldn't just increase your code coverage percentage. Make sure they are useful! Don't just write tests for the sake of writing tests. They are code too, so they need to be maintained.
Example:
from dataclasses import dataclass @dataclass class User: first_name: str last_name: str def fullname(self): return f"{self.first_name} {self.last_name}" # BAD def test_full_name(): user = User(first_name="John", last_name="Doe") assert user.fullname() is not None # GOOD def test_full_name_consists_of_first_name_and_last_name(): first_name = "John" last_name = "Doe" full_name = "John Doe" user = User(first_name=first_name, last_name=last_name) assert user.fullname() == full_name
Test behavior, not implementation
Python clean test tip:
Tests should check the behavior rather than the underlying implementation details.
Such tests are easier to understand and maintain. They're also more resistant to refactoring (helps prevent false negatives).
👇
from dataclasses import dataclass @dataclass class User: username: str class InMemoryUserRepository: def __init__(self): self._users = [] def add(self, user): self._users.append(user) def get_by_username(self, username): return next(user for user in self._users if user.username == username) # BAD def test_add(): user = User(username="johndoe") repository = InMemoryUserRepository() repository.add(user) assert user in repository._users def test_get_by_username(): user = User(username="johndoe") repository = InMemoryUserRepository() repository._users = [user] user_from_repository = repository.get_by_username(user.username) assert user_from_repository == user # GOOD def test_added_user_can_be_retrieved_by_username(): user = User(username="johndoe") repository = InMemoryUserRepository() repository.add(user) assert user == repository.get_by_username(user.username)
Docker - Cache Python Packages to the Docker Host
Docker best practice:
Cache Python packages to the Docker host by mounting a volume or using BuildKit.
Example Dockerfile:
# Mount volume option -v $HOME/.cache/pip-docker/:/root/.cache/pip # BuildKit # syntax = docker/dockerfile:1.2 ... COPY requirements.txt . RUN --mount=type=cache,target=/root/.cache/pip \ pip install -r requirements.txt ...
Serving files with Python's HTTP server
Python tip:
When you need to just serve your static files inside a folder you can do that with Python's HTTP server:
$ cat index.html <html> <h1>Website Prototype</h1> <h2>List of Users:</h2> <ul> <li>Patrick</li> <li>Jan</li> </ul> </html> $ python3 -m http.server Serving HTTP on :: port 8000 (http://[::]:8000/) ...
Python docstrings examples
Python Clean Code Tip:
Use docstrings to document usage of your modules, classes, and functions.
""" The temperature module: Manipulate your temperature easily Easily calculate daily average temperature """ from typing import List class HighTemperature: """Class representing very high temperatures""" def __init__(self, value: float): """ :param value: value of temperature """ self.value = value def daily_average(temperatures: List[float]) -> float: """ Get average daily temperature Calculate average temperature from multiple measurements :param temperatures: list of temperatures :return: average temperature """ return sum(temperatures) / len(temperatures)
Do not store secrets in plaintext in code
Python Clean Code Tip:
Avoid storing things like secret keys, passwords, connection strings, and API keys inside your code. Instead, use a secrets management solution like AWS Secrets Manager or Vault.
# bad class ProductionConfig: DEBUG = False TESTING = False APP_ENVIRONMENT = "production" SQLALCHEMY_DATABASE_URI = ( "postgresql://my_user:strong_password@my_server:5432/my_db" ) # better import boto3 class ProductionConfig: DEBUG = False TESTING = False APP_ENVIRONMENT = "production" _SQLALCHEMY_DATABASE_URI = None @property def SQLALCHEMY_DATABASE_URI(self): if self._SQLALCHEMY_DATABASE_URI is None: self._SQLALCHEMY_DATABASE_URI = boto3.client( "secretsmanager" ).get_secret_value(SecretId=f"db-connection-string-{self.APP_ENVIRONMENT}")[ "SecretString" ] return self._SQLALCHEMY_DATABASE_URI
If a secrets management tool is overkill for your project, store secrets in environment variables. Never store them in plaintext in your code.
Python - use real objects over primitive types
Python Clean Code Tip:
Favor real objects over primitive types such as dictionaries.
Why?
- It's easier to type
user.name
rather thanuser['name']
- You'll get help from your IDE
- You can actually check your code before it runs with mypy
- It makes your code more clear
# bad user = {"first_name": "John", "last_name": "Doe"} full_name = f"{user['first_name']} {user['last_name']}" print(full_name) # => John Doe # better class User: def __init__(self, first_name, last_name): self.first_name = first_name self.last_name = last_name def full_name(self): return f"{self.first_name} {self.last_name}" user = User(first_name="John", last_name="Doe") print(user.full_name()) # => John Doe
Python - find minimum value using special comparator
Python Clean Code Tip:
Use
min
to find an element with minimal value inside an iterable. You can provide a custom function as akey
argument to serve as a key for the min comparison.temperatures = [22.3, 28.7, 15.3, 18.2] # without min min_temperature = 10000 for temperature in temperatures: if temperature < min_temperature: min_temperature = temperature print(min_temperature) # => 15.3 # with min min_temperature = min(temperatures) print(min_temperature) # => 15.3 # using key users = [ {"username": "johndoe", "height": 1.81}, {"username": "marrydoe", "height": 1.69}, {"username": "joedoe", "height": 2.03}, ] shortest_user = min(users, key=lambda user: user["height"]) print(shortest_user) # {'username': 'marrydoe', 'height': 1.69}
Be consistent with the order of your parameters
Python Clean Code Tip:
Be consistent with order of parameters for similar functions/methods. Don't confuse your readers.
# bad def give_first_dose_of_vaccine(person, vaccine): print(f"Give first dose of {vaccine} to {person}.") def give_second_dose_of_vaccine(vaccine, person): print(f"Give second dose of {vaccine} to {person}.") give_first_dose_of_vaccine("john", "pfizer") # Give first dose of pfizer to john. give_second_dose_of_vaccine("jane", "pfizer") # Give second dose of jane to pfizer. # good def give_first_dose_of_vaccine(person, vaccine): print(f"Give first dose of {vaccine} to {person}.") def give_second_dose_of_vaccine(person, vaccine): print(f"Give second dose of {vaccine} to {person}.") give_first_dose_of_vaccine("john", "pfizer") # Give first dose of pfizer to john. give_second_dose_of_vaccine("jane", "pfizer") # Give second dose of pfizer to jane.
Python - High-precision calculations with Decimal
Python Clean Code Tip:
Avoid using floats when you need precise results. Use
Decimal
instead.e.g. prices
👇
from dataclasses import dataclass # bad from decimal import Decimal @dataclass class Product: price: float print(Product(price=0.1 + 0.2)) # => Product(price=0.30000000000000004) # good @dataclass class Product: price: Decimal print(Product(price=Decimal("0.1") + Decimal("0.2"))) # => Product(price=Decimal('0.3'))
Python - OOP tip: set attributes in the constructor
Python Clean Code Tip:
Avoid setting attributes of your objects outside of the constructor. Instead, implement methods that map to real-world concepts.
Why?
To ensure attributes exist and are easily discoverable.
👇
from dataclasses import dataclass from enum import Enum from uuid import UUID class OrderStatus(str, Enum): PLACED = "PLACED" CANCELED = "CANCELED" FULFILLED = "FULFILLED" # bad @dataclass class Order: status: OrderStatus class CancelOrder: def __init__(self, order_repository): self.order_repository = order_repository def execute(self, order_id: UUID): order = self.order_repository.get_by_id(order_id) order.status = OrderStatus.CANCELED self.order_repository.save(order) # better class Order: def __init__(self, status: OrderStatus): self._status = status def cancel(self): self._status = OrderStatus.CANCELED class CancelOrder: def __init__(self, order_repository): self.order_repository = order_repository def execute(self, order_id: UUID): order = self.order_repository.get_by_id(order_id) order.cancel() self.order_repository.save(order)
Python - OOP tip: avoid using too many attributes on a single object
Python Clean Code Tip:
Avoid using too many attributes on a single object. Try to cluster them to improve cohesion, reduce coupling, and improve readability
👇
import datetime from dataclasses import dataclass # bad @dataclass class ExcelSheet: file_name: str file_encoding: str document_owner: str document_read_password: str document_write_password: str creation_time: datetime.datetime update_time: datetime.datetime # good @dataclass class FileProperties: name: str encoding: str @dataclass class SecurityProperties: owner: str read_password: str write_password: str @dataclass class DocumentDating: creation_time: datetime.datetime update_time: datetime.datetime @dataclass class ExcelSheet: file_properties: FileProperties security_properties: SecurityProperties document_dating: DocumentDating
Do not use bare except
Python Clean Code Tip:
Avoid empty except blocks -> try-except-pass.
They lead to hard-to-find bugs.
👇
# bad import logging def send_email(): print("Sending email") raise ConnectionError("Oops") try: send_email() except: # AVOID THIS pass # better logger = logging.getLogger(__name__) try: send_email() except ConnectionError as exc: logger.error(f"Cannot send email {exc}")
Python - use all uppercase for constants
Python Clean Code Tip:
Use upper case names for constants
👇
from typing import Final MAX_NUMBER_OF_RETRIES: Final = 666 class Driver: MAX_HEIGHT: Final = 190
Python type annotation specificity
Python tip:
Specify the most general type for inputs and the most specific type for outputs.
For example:
from typing import List def sum_of_elements(elements: List[int]) -> int: sum_el = 0 for element in elements: sum_el += element return sum_el print(sum_of_elements((9, 9))) """ $ mypy example.py example.py:13: error: Argument 1 to "sum_of_elements" has incompatible type "Tuple[int, int]"; expected "List[int]" Found 1 error in 1 file (checked 1 source file) """ from typing import Iterable def sum_of_elements(elements: Iterable[int]) -> int: sum_el = 0 for element in elements: sum_el += element return sum_el print(sum_of_elements((9, 9))) """ $ mypy example.py Success: no issues found in 1 source file """
Python: Check if an iterable contains a specific element
Python Clean Code Tip:
Use
in
to check whether an iterable contains a specific element.👇
lucky_numbers = [1, 23, 13, 1234] BEST_NUMBER = 13 # without in best_number_is_lucky_number = False for number in lucky_numbers: if number == BEST_NUMBER: best_number_is_lucky_number = True print(best_number_is_lucky_number) # => True # with in best_number_is_lucky_number = BEST_NUMBER in lucky_numbers print(best_number_is_lucky_number) # => True
Python type hints - descriptive variable names
Python Clean Code Tip:
Avoid using the variable/parameter type inside your variable/parameter name. Use type hints instead.
# BAD: user_list # GOOD: users: list[User]
Full example👇
from dataclasses import dataclass @dataclass class User: username: str # bad def print_users(user_list): for user in user_list: print(user.username) print_users([User(username="johndoe")]) # => johndoe # good def print_users(users: list[User]): for user in users: print(user.username) print_users([User(username="johndoe")]) # => johndoe
Python - avoid HTTP status code magic numbers with http.HTTPStatus()
Python Clean Code Tip:
Use
HTTPStatus
fromhttp
(it's inside the standard library) to avoid "magic" numbers for statuses inside your code.Example:
from http import HTTPStatus from fastapi import FastAPI app = FastAPI() @app.get("/old", status_code=200) async def old(): return {"message": "Hello World"} @app.get("/", status_code=HTTPStatus.OK) async def home(): return {"message": "Hello World"}
Python - splitting a module into multiple files
Python Clean Code Tip:
When your module becomes too big you can restructure it to a package while keeping all the imports from the module as they were.
👇
# BEFORE # models.py class Order: pass class Shipment: pass # └── models.py # AFTER # change to package # models/__init__.py from .order import Order from .shipment import Shipment __all__ = ["Order", "Shipment"] # models/order.py class Order: pass # models/shipment.py class Shipment: pass # └── models # ├── __init__.py # ├── order.py # └── shipment.py # imports from module/package can stay the same from models import Order, Shipment
Design by contract in Python - preconditions
Python Clean Code Tip:
Use preconditions to ensure the integrity of your objects.
For example:
class Date: def __init__(self, day, month, year): self.day = day self.month = month self.year = year startDate = Date(3, 11, 2020) # OK startDate = Date(31, 13, 2020) # this one should fail since there are only 12 months class Date: LAST_MONTH = 12 LAST_DAY = 31 def __init__(self, day, month, year): if month > self.LAST_MONTH: raise Exception(f"Month cannot be greater than {self.LAST_MONTH}") if day > self.LAST_DAY: raise Exception(f"Day cannot be greater than {self.LAST_DAY}") self.day = day self.month = month self.year = year startDate = Date(3, 11, 2020) # OK startDate = Date(31, 13, 2020) # this one fails # DISCLAIMER: production ready validation should be more complex since not all months have 31 days
Operator Overloading in Python
Python Clean Code Tip:
Use operator overloading to enable usage of operators such as
+
,-
,/
,*
, ... on your instances.👇
from dataclasses import dataclass # without operator overloading @dataclass class TestDrivenIOCoin: value: float def add(self, other): if not isinstance(other, TestDrivenIOCoin): return NotImplemented return TestDrivenIOCoin(value=self.value + other.value) my_coins = TestDrivenIOCoin(value=120).add(TestDrivenIOCoin(value=357.01)) print(my_coins) # TestDrivenIOCoin(value=477.01) # with operator overloading @dataclass class TestDrivenIOCoin: value: float def __add__(self, other): if not isinstance(other, TestDrivenIOCoin): return NotImplemented return TestDrivenIOCoin(value=self.value + other.value) my_coins = TestDrivenIOCoin(value=120) + TestDrivenIOCoin(value=357.01) print(my_coins) # TestDrivenIOCoin(value=477.01)
Chaining comparison operators in Python
Python Clean Code Tip:
Use chained comparison when you need to check whether some variable is between MIN and MAX values.
👇
from dataclasses import dataclass @dataclass class SurfBoard: width: float length: float MINIMAL_LENGTH = 201.3 MAXIMAL_LENGTH = 278.5 # without chained comparison def board_is_pwa_compliant(surf_board: SurfBoard): return surf_board.length > MINIMAL_LENGTH and surf_board.length < MAXIMAL_LENGTH surf_board = SurfBoard(width=75.3, length=202.7) print(board_is_pwa_compliant(surf_board)) # True # with chained comparison def board_is_pwa_compliant(surf_board: SurfBoard): return MINIMAL_LENGTH < surf_board.length < MAXIMAL_LENGTH print(board_is_pwa_compliant(surf_board)) # True # don't abuse it like this: a <= b < c > d
__all__ in Python
Python Clean Code Tip:
Use
__all__
to define exported members of your package.Hint: IDEs will do a much better job at importing and autocomplete.
from .my_module import my_function __all__ = ["my_function"]
Python - built-in sum function vs. for loop
Python Clean Code Tip:
Use
sum
to sum the values of all elements inside an iterable instead of afor
loop.Why?
- Don't re-invent the wheel!
sum
is much faster👇
transactions = [10.0, -5.21, 101.32, 1.11, -0.38] # without sum balance = 0 for transaction in transactions: balance += transaction # with sum balance = sum(transactions)
Python - Reduce Boilerplate Code with Dataclasses
Python Clean Code Tip:
Use dataclasses when only storing attributes inside your class instances to reduce the amount of boilerplate code.
For example:
# without dataclass class Address: def __init__(self, street, city, zip_code): self.street = street self.city = city self.zip_code = zip_code def __repr__(self): return ( f"Address(street={self.street}, city={self.city}, zip_code={self.zip_code})" ) def __hash__(self) -> int: return hash((self.street, self.city, self.zip_code)) def __eq__(self, other) -> bool: if not isinstance(other, Address): return NotImplemented return (self.street, self.city, self.zip_code) == ( other.street, other.city, other.zip_code, ) # with dataclass from dataclasses import dataclass @dataclass(unsafe_hash=True) class Address: street: str city: str zip_code: str
Check for code quality issues inside your CI/CD pipelines
Python Clean Code Tip:
Check the quality of your code inside your CI pipeline.
Use:
- flake8 - style guide enforcer
- black - code formatting
- isort - optimize imports
- bandit - check for security vulnerabilities
- safety - check for security vulnerabilities of dependencies
Github Actions Example 👇
name: Check code quality on: [push] jobs: code-quality: strategy: fail-fast: false matrix: python-version: [3.9] poetry-version: [1.1.8] os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - uses: with: actions/setup-python@v2 python-version: ${{ matrix. python-version }} - name: Run image uses: abatilo/[email protected] with: poetry-version: ${{ matrix. poetry-version }} - name: Install dependencies run: poetry install - name: Run black run: poetry run black . --check - name: Run isort run: poetry run isort . --check-only --profile black - name: Run flake8 run: poetry run flake8 . - name: Run bandit run: poetry run bandit . - name: Run saftey run: poetry run safety check
It's a good idea to couple this with pre-commit hooks:
- pre-commit - format code with black and isort
- CI pipeline - run black and isort with check flags to ensure that code has been properly formatted
In other words, you shouldn't actually format any code in the CI pipeline. You just want to verify that formatting happened via pre-commit.
Don't use flags in functions
Python Clean Code Tip:
Don't use flags in functions.
Flags are variables passed to functions, which the function uses to determine its behavior. This pattern should be avoided since functions should only perform a single task. If you find yourself doing this, split your function into smaller functions.
👇
text = "This is a cool blog post" # This is bad def transform(text, uppercase): if uppercase: return text.upper() else: return text.lower() # This is good def uppercase(text): return text.upper() def lowercase(text): return text.lower()
Python Clean Code: Keep your function arguments at a minimum
Python Clean Code Tip:
Keep your arguments at a minimum.
Ideally, your functions should only have one to two arguments. If you need to provide more arguments to the function, you can create a config object which you pass to the function or split it into multiple functions.
Example:
# This is bad def render_blog_post(title, author, created_timestamp, updated_timestamp, content): # ... render_blog_post("Clean code", "Nik Tomazic", 1622148362, 1622148362, "...") # This is good class BlogPost: def __init__(self, title, author, created_timestamp, updated_timestamp, content): self.title = title self.author = author self.created_timestamp = created_timestamp self.updated_timestamp = updated_timestamp self.content = content blog_post1 = BlogPost("Clean code", "Nik Tomazic", 1622148362, 1622148362, "...") def render_blog_post(blog_post): # ... render_blog_post(blog_post1)
Functions should only perform a single task
Python Clean Code Tip:
Functions should only perform a single task
Hint: If your function contains the keyword 'and' you can probably split it into two functions.
# This is bad def fetch_and_display_personnel(): data = # ... for person in data: print(person) # This is good def fetch_personnel(): return # ... def display_personnel(data): for person in data: print(person)
Clean code tip - Don't add unnecessary context
Python Clean Code Tip:
Don't add redundant context.
Do not add unnecessary data to variable names, especially if you're working with classes.
# This is bad class Person: def __init__(self, person_first_name, person_last_name, person_age): self.person_first_name = person_first_name self.person_last_name = person_last_name self.person_age = person_age # This is good class Person: def __init__(self, first_name, last_name, age): self.first_name = first_name self.last_name = last_name self.age = age
We're already inside the
Person
class, so there's no need to add aperson_
prefix to every class variable.
Clean code tip - Don't use magic numbers
Python Clean Code Tip:
Don't use "magic numbers".
Magic numbers are strange numbers that appear in code, which do not have a clear meaning.
👇
import random # This is bad def roll(): return random.randint(0, 36) # what is 36 supposed to represent? # This is good ROULETTE_POCKET_COUNT = 36 def roll(): return random.randint(0, ROULETTE_POCKET_COUNT)
Instead of using magic numbers, extract them into a meaningful variable.
Clean code tip - Avoid using ambiguous abbreviations
Python clean code tip:
Avoid using ambiguous abbreviations
Don't try to come up with your own abbreviations. It's better for a variable to have a longer name than a confusing name.
👇
# This is bad fna = 'Bob' cre_tmstp = 1621535852 # This is good first_name = 'Bob' creation_timestamp = 1621535852
Queryset.explain() in Django
Django tip:
If you want to know how the database would execute a given query, you can use
explain()
.Knowing this can be helpful when you're trying to improve the performance of slow queries.
>>> print(Payment.objects.filter(created_at__gt=datetime.date(2021, 1, 1)).explain()) Seq Scan on payments_payment (cost=0.00..14.25 rows=113 width=212) Filter: (created_at > '2021-01-01 00:00:00+00'::timestamp with time zone)
Check if a file is a symlink in Python
Python tip:
You can use pathlib's
is_symlink()
to check whether a path is a symlink.👇
import pathlib path = pathlib.Path("/usr/bin/python") print(path.is_symlink()) # => True
Python - slice a generator object
Python tip:
You can use itertools.islice to use only part of a generator.
👇
from itertools import cycle, islice chord_sequence = cycle(["G", "D", "e", "C"]) song_chords = [chord for chord in islice(chord_sequence, 16)] print(song_chords) """ ['G', 'D', 'e', 'C', 'G', 'D', 'e', 'C', 'G', 'D', 'e', 'C', 'G', 'D', 'e', 'C'] """
Positional-only arguments in Python
Did you know?
You can force a user to call a function with positional arguments only using
/
.Example:
def full_name(user, /): return f"{user['first_name']} {user['last_name']}" print(full_name({"first_name": "Jan", "last_name": "Giamcomelli"})) # => Jan Giamcomelli print(full_name(user={"first_name": "Jan", "last_name": "Giamcomelli"})) # => TypeError: full_name() got some positional-only arguments passed as keyword arguments: 'user'
Why?
Makes refactoring easier. You can change the name of your parameters without worrying about it breaking any code that uses the function.