Problem solving - what have you tried?


When stuck, spend at least 30 minutes but no more than 60 trying to solve the problem before asking for help. Also, when you ask for help make sure you can answer the question, "What have you tried?”.

Python's platform module


Python tip:

You can check the type of machine and platform that your program is running from with platform.machine() and platform.platform(), respectively.

An example👇

import platform

print(platform.machine())
# => x86_64

print(platform.platform())
# => macOS-10.15.6-x86_64-i386-64bit

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 the print() 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?

  1. Autocomplete
  2. 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:[email protected]_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?

  1. It's easier to type user.name rather than user['name']
  2. You'll get help from your IDE
  3. You can actually check your code before it runs with mypy
  4. 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 a key 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 from http (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 a for loop.

Why?

  1. Don't re-invent the wheel!
  2. 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:

  1. flake8 - style guide enforcer
  2. black - code formatting
  3. isort - optimize imports
  4. bandit - check for security vulnerabilities
  5. 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/[email protected]
      - uses:
        with:   actions/[email protected]
           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 a person_ 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.

all() in Python


Python tip:

You can use all to check whether all values inside an iterable are truthy.

👇

numbers = [10, 99, 321, 3]
print(all(number > 0 for number in numbers))
# => True

print(all([]))
# => True

numbers = [-1, 2, 5, 10, 0]
print(all(number > 0 for number in numbers))
# => False

any() in Python


Python tip:

You can use any to check whether any element inside an iterable has a truthy value.

An example👇

platforms = ["Facebook", "Twitter", "Instagram"]

print(any(platform == "Twitter" for platform in platforms))
# => True

print(any([]))
# => False

print(any([2, 3, 4]))
# => True

Parse datetime strings in Python with dateutil


Did you know?

You can use dateutil.parser to parse all sorts of different date and datetime string formats.

https://pypi.org/project/python-dateutil/

An example👇

from dateutil.parser import parse

print(parse("2012-01-19 17:21:00"))
# => 2012-01-19 17:21:00

print(parse("1st Jan, 2019"))
# => 2019-01-01 00:00:00

print(parse("23.11.2020"))
# => 2020-11-23 00:00:00

Unfortunately, this is quite slow:

from dateutil.parser import parse
%timeit parse(datetime_txt).date()
54.3 µs ± 450 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

import datetime
%timeit datetime.datetime.strptime(datetime_txt, "%H:%M %d-%m-%Y").date()
7.44 µs ± 240 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Python @classmethod


Python tip:

You can use @classmethod to create class methods.

For example, you can create a class method that loads events from a JSON string message👇

import datetime
import json


class UserRegistered:
    def __init__(self, username, event_time):
        self.username = username
        self.event_time = event_time

    @classmethod
    def from_event_message(cls, message):
        message = json.loads(message)

        return cls(
            username=message["username"],
            event_time=datetime.datetime.fromisoformat(message["event_time"]),
        )


message = '{"username": "johndoe", "event_time": "2021-04-26T20:00:00"}'
event = UserRegistered.from_event_message(message)
print(event.username, event.event_time)
# => johndoe 2021-04-26 20:00:00

5 awesome tools for writing Python tests


5 awesome tools for writing Python tests:

  1. pytest - go-to testing framework for testing Python code
  2. unittest.mock - built-in mocking library
  3. coverage.py - measuring code coverage (use pytest-cov plugin when using pytest)
  4. mutmut - improve the quality of your test suite with mutation testing
  5. hypothesis - property-based testing library

You can learn about all of these tools here.

How do you parameterize a test in pytest?


Pytest tip:

You can parametrize your tests with @pytest.mark.parametrize.

Example:

import pytest


def is_palindrome(text):
    return text == "".join(reversed(text))


@pytest.mark.parametrize(
    "text, is_pal",
    [
        ("kayak", True),
        ("racecar", True),
        ("bmw", False),
        ("songs", False),
    ],
)
def test_is_palindrome(text, is_pal):
    assert is_palindrome(text) == is_pal

pydantic typecasting


Did you know?

Pydantic uses typecasting.

That means that it tries to convert provided values to their expected types if it's possible.

Examples👇

import datetime

from pydantic import BaseModel


class Location(BaseModel):
    city: str
    country: str


class Measurement(BaseModel):
    temperature: float
    measured_at: datetime.datetime
    location: Location


print(
    Measurement(
        temperature=10,
        measured_at="2021-04-16T21:15:03.000012",
        location={"city": "London", "country": "UK"},
    )
)

"""
temperature=10.0 measured_at=datetime.datetime(2021, 4, 16, 21, 15, 3, 12) location=Location(city='London', country='UK')
"""

print(
    Measurement(
        temperature=10.0,
        measured_at="2021-04-16T21:15:03",
        location=Location(city="London", country="UK"),
    )
)

"""
temperature=10.0 measured_at=datetime.datetime(2021, 4, 16, 21, 15, 3) location=Location(city='London', country='UK')
"""

print(
    Measurement(
        temperature=10,
        measured_at="16186000528",
        location=[("city", "London"), ("country", "UK")],
    )
)

"""
temperature=10.0 measured_at=datetime.datetime(2021, 4, 16, 19, 15, 28, tzinfo=datetime.timezone.utc) location=Location(city='London', country='UK')
"""

Public, protected, and private methods ​in Python


Did you know?

Python doesn't have public, protected, or private methods.

Instead, use the following naming conventions:

  1. method_name -> public
  2. _method_name -> protected
  3. __method_name -> private

The same goes for attributes.

An example👇

class User:
    def __init__(self, first_name, last_name, age):
        self.first_name = first_name
        self.last_name = last_name
        self.__age = age

    def age(self):
        return "You shouldn't ask that. It's a private information."

    def _is_adult(self):
        return self.__age >= 21

    def can_drink_alcohol(self):
        return self._is_adult()

Python - checking whether a list contains duplicates


Did you know?

As long as the elements are hashable, you can check whether a list contains duplicated values by converting it to a set and comparing the lengths of the list and set.

Example:

names = ["Jan", "John", "Marry", "Daisy", "Jan"]

names_has_duplicates = len(set(names)) < len(names)
print(names_has_duplicates)
# => True

Black compatibility with flake8


Python tip:

When using black and flake8 together set the max line length for flake8 to 88 to match black's default value.

Minimal flake8 black compatible config:

[flake8]
max-line-length = 88
extend-ignore = E203

Python 3.10 - parenthesized context managers


Did you know?

Python 3.10 comes with support for continuation across multiple lines when using context managers.

An example👇

import unittest
from unittest import mock

from my_module import my_function


# Python < 3.10


class Test(unittest.TestCase):
    def test(self):
        with mock.patch("my_module.secrets.token_urlsafe") as a, mock.patch("my_module.string.capwords") as b, mock.patch("my_module.collections.defaultdict") as c:
            my_function()
            a.assert_called()
            b.assert_called()
            c.assert_called()

    def test_same(self):
        with mock.patch("my_module.secrets.token_urlsafe") as a:
            with mock.patch("my_module.string.capwords") as b:
                with mock.patch("my_module.collections.defaultdict") as c:
                    my_function()
                    a.assert_called()
                    b.assert_called()
                    c.assert_called()


# Python >= 3.10

class Test(unittest.TestCase):
    def test(self):
        with (
            mock.patch("my_module.secrets.token_urlsafe") as a,
            mock.patch("my_module.string.capwords") as b,
            mock.patch("my_module.collections.defaultdictl") as c,
        ):
            my_function()
            a.assert_called()
            b.assert_called()
            c.assert_called()

Difference between re.search and re.match in Python?


re.match() searches for matches from the beginning of a string while re.search() searches for matches anywhere in the string.

Example:

import re

claim = 'People love Python.'

print(re.search(r'Python', claim).group())
# => Python

print(re.match(r'Python', claim))
# => None

print(re.search(r'People', claim).group())
# => People

print(re.match(r'People', claim).group())
# => People

Extract contents of a PDF file with pikepdf in Python


Python tip:

You can use pikepdf to extract a subset pf pages from an original PDF and create a new file containing only the extracted pages.

For example,👇 save pages 2 and 3 from the original PDF

import pathlib

from pikepdf import Pdf

start = 2
stop = 3
filepath = pathlib.Path("file.pdf")

pdf = Pdf.open(filepath)
new_pdf = Pdf.new()

new_pdf.pages.extend(pdf.pages[start - 1 : stop])

new_pdf.save("new.pdf", compress_streams=False)

Using pathlib's read_bytes method in Python


Python tip:

You can use read_bytes() (which handles the opening and closing of the file) from pathlib's Path to read bytes from a file instead of using with open().

Example:

# pathlib
import pathlib
file_bytes = pathlib.Path("file.pdf").read_bytes()


# with open
with open("file.pdf", "rb") as file:
    file_bytes = file.read()

Python - provide a function as an argument to another function to improve its testability


Python tip:

You can provide a function as an argument to another function to improve its testability.

For example, you can provide input as a function in a real application and mock the function in test

def get_user_expense(get_input):
    raw_input = get_input(
        "Enter your expense (date;description;amount) (e.g. 2021-03-21;Gasoline;75.43): "
    )
    date, description, amount = raw_input.split(";")
    return date, description, amount


# test
def test_get_user_expense():
    assert get_user_expense(lambda *args: "2021-03-21;Gasoline;75.43") == (
        "2021-03-21",
        "Gasoline",
        "75.43",
    )


# real application
def main():
    date, description, amount = get_user_expense(input)
    print(date, description, amount)

Poetry - generate requirements.txt without hashes


Poetry tip:

When using Docker, you can export dependencies to a requirements.txt file without hashes to decrease time to resolve dependencies.

👇

poetry export --without-hashes --format=requirements.txt > requirements.txt

Caching poetry install for CI


Poetry tip:

When running poetry inside a CI pipeline, set virtualenvs.in-project config to true.

That way the virtual environment will be created inside the current folder -> you can cache it.

Gitlab example👇

stages:
  - test

cache:
  key: virtualenv
  paths:
    - .venv/

tests:
  stage: test
  image: python3.8-slim
  before_script:
    - poetry config virtualenvs.in-project true
    - poetry install
  script:
    - poetry run python -m pytest tests

Black compatibility with isort


Python tip:

When using black and isort on the same project, use isort's profile black to avoid code formatting conflicts

👇

$ isort . --profile black

Python dictionary clear() method


Python tip:

You can use .clear to clear a dictionary.

Is it the same as foo = {} ?

No.

  1. foo = {} will create a new instance but other references will still point to the old items.
  2. Because of #1, foo = {} is probably faster than foo.clear()
  3. foo.clear() is slower but safer. Be careful with scope with foo = {}.

An example👇

user = {"name": "Jan", "username": "giaco"}

user.clear()
print(user). # => {}

What’s the difference between '==' and 'is' in Python?


What's the difference between == and is?

Let's find out👇

== is an equality operator. It checks whether two objects are equal by their values. To compare, objects must have an __eq__ method implemented.

my_list = [1, 2, 3]
another_list = [element for element in my_list]
print(my_list == another_list)
# => True


class User:
    def __init__(self, name):
        self.name = name

    def __eq__(self, other):
        return other.name == self.name


user1 = User(name="Jan")
user2 = User(name="Jan")
print(user1 == user2)
# => True

is is an identity operator. It checks whether you're referencing the same object on both sides of the comparison. It doesn't care about the values.

my_list = [1, 2, 3]
another_list = [element for element in my_list]
print(my_list is another_list)
# => False


class User:
    def __init__(self, name):
        self.name = name


user1 = User(name="Jan")
user2 = User(name="Jan")
print(user1 is user2)
# => False
print(user1 is user1)
# => True

When should you use one over the other?

Use== when you care about values and use is when you care about actual objects.

Python - assert that a mock function was called with certain arguments


Python tip:

You can assert that the method on a MagicMock object was called with certain arguments.

https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_called_with

For example, you can mock an email service and assert that the send method was called with specific values:

from unittest.mock import MagicMock


def place_order(user, items, email_service):
    print(f"Place order for user {user}")
    print(f"With items: {items}")
    email_service.send(user, "Order placed.")


def test_place_order():
    email_service = MagicMock()

    place_order("[email protected]", ["Computer", "USB drive", "Vodka"], email_service)
    email_service.send.assert_called_with("[email protected]", "Order placed.")

Python - unittest.mock.create_autospec() example


Python tip:

You can use mock.create_autospec to create a mock object with the same interface as the provided class, function, or method.

For example, you can use it to mock Response from the requests package👇

from unittest import mock

import requests
from requests import Response


def get_my_ip():
    response = requests.get(" http://ipinfo.to/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

Getting started with Python type hints


Python tip:

Use type hints to annotate expected types for variables, function parameters, and function returns.

https://testdriven.io/blog/python-type-checking/

An example👇

from typing import Collection


def daily_average(temperatures: Collection[float]) -> float:
    return sum(temperatures) / len(temperatures)


print(daily_average.__annotations__)
# => {'temperatures': typing.Collection[float], 'return': <class 'float'>}

Teaching kids programming with Python Turtle


Python tip:

Turtle graphics is a popular way to introduce programming to kids.

You can use it to draw different shapes. The cursor moves on the screen.

An example👇

from turtle import *

color("red", "yellow")
begin_fill()

while True:
    forward(200)
    left(170)
    if abs(pos()) < 1:
        break

end_fill()
done()

Django deployment checklist


Django tip:

Check your production settings.py file for security vulnerabilities with the check command:

$ ./manage.py check --deploy

https://docs.djangoproject.com/en/3.2/ref/django-admin/#check

Example:

$ ./manage.py check --deploy
System check identified some issues:

WARNINGS:
have not set CSRF COOKIE SECURE to True. Using a secure-only CSRF cookie
makes it more difficult for network traffic sniffers to steal the CSRF token.
? (security.W018) You should not have DEBUG set to True in deployment.
? (security.W022) You have not set the SECURE_REFERRER_POLICY setting.
Without this, your site will not send a Referrer-Policy header.
You should consider enabling this header to protect user privacy.

Test and publish your Python packages to PyPI with poetry and GitHub Actions


Python tip:

Use poetry and Github Actions to:

  1. run tests for your packages - on every push
  2. check code quality - on every push
  3. publish the packages to PyPI - on every release

https://testdriven.io/blog/python-project-workflow/

An example👇

Test and check for code quality:

name: Push
on: [push]

jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        python-version: [3.9]
        poetry-version: [1.1.2]
        os: [ubuntu-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/[email protected]
      - uses: actions/[email protected]
        with:
          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 tests
        run: poetry run pytest --cov=./ --cov-report=xml
      - name: Upload coverage to Codecov
        uses: codecov/[email protected]
  code-quality:
    strategy:
      fail-fast: false
      matrix:
        python-version: [3.9]
        poetry-version: [1.1.2]
        os: [ubuntu-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/[email protected]
      - uses: actions/[email protected]
        with:
          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
      - name: Run flake8
        run: poetry run flake8 .
      - name: Run bandit
        run: poetry run bandit .
      - name: Run saftey
        run: poetry run safety check

Release:

name: Release
on:
  release:
    types:
      - created

jobs:
  publish:
    strategy:
      fail-fast: false
      matrix:
        python-version: [3.9]
        poetry-version: [1.1.2]
        os: [ubuntu-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/[email protected]
      - uses: actions/[email protected]
        with:
          python-version: ${{ matrix.python-version }}
      - name: Run image
        uses: abatilo/[email protected]
        with:
          poetry-version: ${{ matrix.poetry-version }}
      - name: Publish
        env:
          PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
        run: |
          poetry config pypi-token.pypi $PYPI_TOKEN
          poetry publish --build

Unpacking iterables in Python


Python tip:

You can unpack any iterable to variables as long as the number of variables is the same as the number of elements in the iterable.

Examples👇

a, b, c = (1, 2, 3)
print(a, b, c)
# => 1 2 3

a, b, c, d = (1, 2, 3, 4)
print(a, b, c, d)
# => 1 2 3 4

a, b = {1, 2}
print(a, b)
# => 1 2

a, b = {'a': 1, 'b': 2}
print(a, b)
# => a b

a, b, c = (i for i in range(1, 4))
print(a, b, c)
# => 1 2 3

a, b, c = (1, 2)
print(a, b, c)
# => ValueError: not enough values to unpack (expected 3, got 2)

a, b, c = (1, 2, 3, 4)
print(a, b, c)
# => ValueError: too many values to unpack (expected 3)

Finding memory leaks in Python with tracemalloc


Python tip:

You can use tracemalloc to display files allocating the most memory.

An example👇

import tracemalloc

import app

tracemalloc.start()

app.run()

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics("lineno")

for stat in top_stats[:10]:
    print(stat)

Python - quick tkinter example


Python tip:

You can use tkinter to build simple desktop applications

An example👇

import tkinter as tk


class Application(tk.Frame):
    def __init__(self, master=None):
        self.number_of_clicks = tk.IntVar(value=0)

        super().__init__(master)
        self.master = master
        self.pack()
        self.create_widgets()

    def create_widgets(self):
        self.number_of_clicks_value = tk.Label(self, textvariable=self.number_of_clicks)
        self.number_of_clicks_value.pack(side="top")

        self.click_me = tk.Button(self, text="Click me", command=self.increment)
        self.click_me.pack(side="top")

        self.quit = tk.Button(self, text="QUIT", fg="red", command=self.master.destroy)
        self.quit.pack(side="bottom")

    def increment(self):
        self.number_of_clicks.set(self.number_of_clicks.get() + 1)


root = tk.Tk()
app = Application(master=root)
app.mainloop()

Call a function after some interval in Python with Timer


Python tip:

You can use Timer to run some function only after a certain amount of time has passed

An example👇

from threading import Timer


def truth():
    print("Python rocks!")


t = Timer(15, truth)
t.start() # truth will be called after a 15 second interval

Mutable default parameter values in Python


Python tip:

Avoid setting mutable default parameters for functions. Instead, use None as the default and assign the mutable value inside the function.

Why?

Python evaluates a function's default parameters once, not each time the function is called. So, if you allow a mutable default parameter and mutate it, it will be mutated for all future calls.

An example👇

# wrong
def add_jan(users=[]):
    users.append("Jan")
    return users


print(add_jan())  # => ['Jan']
print(add_jan())  # => ['Jan', 'Jan']


# right
def add_jan(users=None):
    users = users or []
    users.append("Jan")
    return users


print(add_jan())  # => ['Jan']
print(add_jan())  # => ['Jan']

Textwrap - text wrapping in Python


Python tip:

You can wrap long strings into multiple lines where each line is shorter than the limit by using textwrap.wrap.

An example👇

import textwrap

text = (
    "The single-responsibility principle (SRP) is a computer programming principle "
    "that states that every class in a computer program should have responsibility over "
    "a single part of that program's functionality, which it should encapsulate. "
    "All of the module's, class' or function's services should be narrowly aligned with that responsibility"
)
lines = textwrap.wrap(text, 70)

for line in lines:
    print(line)

"""
The single-responsibility principle (SRP) is a computer programming
principle that states that every class in a computer program should
have responsibility over a single part of that program's
functionality, which it should encapsulate. All of the module's,
class' or function's services should be narrowly aligned with that
responsibility
"""

How do I extract a tar file in Python?


Python tip:

You can extract a tar archive to a destination folder using tarfile.

An example👇

import tarfile

with tarfile.open("example.tar.gz") as tar:
    tar.extractall("destination_folder")

Python - execute shell commands in subprocess


Python tip:

Use the subprocess module to spawn new processes.

https://docs.python.org/3/library/subprocess.html#subprocess.Popen

Use Popen.communicate to capture result and errors.

An example👇

import subprocess

proc = subprocess.Popen(
    ["ls", "-la", "/home/johndoe/Images"],
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
)
output, err = proc.communicate()

if err:
    raise Exception(err.decode())

print(output.decode())
"""
drwxrwxr-x   2 johndoe  johndoe    4096 Sep 28 08:17 .
drwxr-xr-x  13 johndoe  johndoe    4096 Sep 28 08:12 ..
-rw-rw-r--   1 johndoe  johndoe  115887 Sep 19 09:15 1_initial_user.png
-rw-rw-r--   1 johndoe  johndoe  115055 Sep 19 09:15 2_change_ban.png
-rw-rw-r--   1 johndoe  johndoe  114587 Sep 19 09:15 4_change_database.png
-rw-rw-r--   1 johndoe  johndoe  188718 Sep 19 09:15 5_srp.png
-rw-rw-r--   1 johndoe  johndoe  188718 Sep 19 09:15 change_charge.png
"""

Arithmetic, Geometric, and Harmonic Means in Python


Python tip:

Use statistics.mean to calculate the arithmetic mean of an iterable.

Use statistics.geometric_mean to calculate the geometric mean of an iterable (Python >= 3.8).

Use statistics.harmonic_mean to calculate the harmonic mean of an iterable.

An example👇

from statistics import geometric_mean, harmonic_mean, mean

print(mean([1, 3, 7, 2, 5]))
# => 3.6

print(geometric_mean([1, 3, 7, 2, 5]))
# => 2.913693458576192

print(harmonic_mean([1, 3, 7, 2, 5]))
# => 2.297592997811816