10.1. Pure Python + Built-in libraries#

10.1.1. Make your numbers more readable#

Do you want to make your numbers in Python more readable?

Use underscores such as 1_000_000.

Underscores in numbers in Python are used to make large numbers more readable.

The underscores are ignored by the interpreter and do not affect the value of the number.

See below for a small example.

Note: Using two consecutive underscores is not allowed!

big_number = 1_000_000_000_000

print(big_number)

10.1.2. Get Query Parameters with urllib.parse#

How to extract query parameters from an URL in Python?

Query parameters are the extra pieces of information you can add to a URL to change how a website behaves.

They come after a ‘q’ character in the URL and are made up of key-value pairs.

The key describes what the information is for and the value is the actual information you want to send.

Query parameters are extra pieces of information that you can add to a URL to change how a website behaves.

To extract those query parameters in Python, use urllib.

urllib provides functions to extract the query and its parameters for you.

from urllib import parse
url = 'https://play.google.com/store/apps/details?id=com.happy.mood.diary&hl=de&gl=US'

# Outputs "id=com.happy.mood.diary&hl=de&gl=US"
query = parse.urlparse(url).query

# Outputs "{'id': ['com.happy.mood.diary'], 'hl': ['de'], 'gl': ['US']}"
parameters = parse.parse_qs(query)

10.1.3. Zip iterables to the longest iterable#

Don’t use zip() in Python.

When you have, let’s say, two lists of unequal length, zip() will return an iterable with as many elements as the shortest list.

Instead, use itertools.zip_longest().

It will “pad” any shorter lists (or other iterables) with a fill value so that the returned iterable has the same length as the longest iterable.

You will not lose any elements!

from itertools import zip_longest

a = [1,2,3,4]
b = [5,6,7]

# zip(): One element is missing
for aa, bb in zip(a, b):
  print(aa, bb)
'''
1 5
2 6
3 7
'''

# zip_longest()
for aa, bb in zip_longest(a, b):
  print(aa, bb)
  
'''
1 5
2 6
3 7
4 None
'''

10.1.4. Improve readability with Named slices#

Do you want to make your code more readable in Python?

Use named slices.

They are reusable and make your code less messy.

Especially when there is a lot of slicing involved.

LETTERS = slice(0,2)
NUMS = slice(2,6)
CITY = slice(6, None)

code_1 = "LH1234 BLN"
code_2 = "LH7672 MUC"

print(code_1[LETTERS], code_1[NUMS], code_1[CITY])
print(code_2[LETTERS], code_2[NUMS], code_2[CITY])

10.1.5. Pythonic way for matrix multiplication#

Did you know the ‘@’ operator performs 𝐦𝐚𝐭𝐫𝐢𝐱 𝐦𝐮𝐥𝐭𝐢𝐩𝐥𝐢𝐜𝐚𝐭𝐢𝐨𝐧 in Python?

Normally, you would use numpy.matmul().

But since Python 3.5, you can also use the ‘@’ operator as a more readable way.

import numpy as np

a =  np.array([[1, 2],
              [4, 1],
              [3, 4]])
b = np.array([[4, 5],
              [1, 0]])

a @ b

np.matmul(a, b)

10.1.6. Use Guard Clauses for better If statements#

Stop nesting your If-statements.

Instead, use the Guard Clause technique.

The Guard Clause pattern says you should terminate a block of code early by checking for invalid inputs or edge cases at the beginning of your functions.

Below you can see how we can make our If-statements more readable by checking for the conditions and immediately return None if it’s false.

# Old way
def calculate_price(quantity, price_per_unit):
    if quantity > 0:
        if price_per_unit > 0:
            return quantity * price_per_unit
        else:
            return None
    else:
        return None
    
# Better way with Guard Clause
def calculate_price(quantity, price_per_unit):
    if quantity <= 0:
        return None
    if price_per_unit <= 0:
        return None
    return quantity * price_per_unit

10.1.7. Hide Password Input from User#

Do your Python Command-line apps include collecting secret keys or passwords?

Use getpass, from the Python Standard Library.

getpass ensures the user’s inputs will not be echoed back to the screen.

A cool tool for your next Command-line app!

import getpass

username = getpass.getuser()

print(f"{username} is logged in.")

password = getpass.getpass()

print(f"Password entered is: {password}")

10.1.8. Turn Classes into Callables#

Did you know you can have callable objects in Python?

You have to override the __call__ method to call the object like a function.

class Example:
  def __call__(self):
    print("I was called!")
    
example_instance = Example()
example_instance() # Will call the __call__ method

10.1.9. Wrap Text with textwrap#

Do you want to format your text output easily?

Use textwrap!

textwrap lets you wrap paragraphs, adjust line widths and handle indentation.

import textwrap

text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec tellus vitae turpis tempus porttitor. Fusce cursus nisi eu urna pharetra, a congue ex aliquet. Quisque vitae consequat nulla, nec bibendum risus."

# Wrap text to a specific line width
wrapped_text = textwrap.wrap(text, width=30)

for line in wrapped_text:
    print(line)

10.1.10. Add LRU Caching to your Functions#

Python offers a neat way to add caching to functions.

You just need to add the lru_cache decorator from functools.

It takes a maxsize argument that specifies the maximum number of results to cache.

When the function is called with the same arguments, it first checks the cache and returns the cached result if available.

from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

fibonacci(10)

10.1.11. Cache Methods in Classes with functools.cached_property#

To cache the result of a method of a class, use functools.cached_property.

It transforms a method into a property where its value is computed only once and then cached.

from functools import cached_property

class MyClass:
    @cached_property
    def expensive_operation(self):
        # Perform some expensive computation here
        return result

my_object = MyClass()

print(my_object.expensive_operation)

10.1.12. For-Else Loops in Python#

You probably know about If-Else.

But do you know about For-Else?

You can add an else-block which will be executed only if the loop completes all its iterations without executing a break statement.

See the small example below, where the else-block will be executed after nothing was found in the for-loop.

fruits = ['apple', 'orange', 'pear']

for fruit in fruits:
    if fruit == 'banana':
        print("Found the banana!")
        break
else:
    print("Banana not found in the list.")

10.1.13. Heaps in Python#

Want an efficient way to retrieve the N smallest/largest elements in Python?

Use 𝐡𝐞𝐚𝐩𝐪.

𝐡𝐞𝐚𝐩𝐪 is a standard library for working with heaps.

A heap is a specialized tree-based data structure that satisfies the heap property.

In a heap, the value of each node is either greater than or equal to (in a max heap) or less than or equal to (in a min heap) the values of its children.

Therefore, retrieving the N smallest/largest elements can be done in a fast way.

import heapq

data = [5, 9, 2, 1, 6, 3, 8, 4, 7]

largest = heapq.nlargest(3, data)
print("Largest elements:", largest)  # Output: [9, 8, 7]

smallest = heapq.nsmallest(3, data)
print("Smallest elements:", smallest)  # Output: [1, 2, 3]

10.1.14. Difference between __str__ and __repr__#

Do you know the difference between __𝐬𝐭𝐫__ and __𝐫𝐞𝐩𝐫__?

In Python, both methods are used to define string representations of an object.

But, when to use which one for your classes?

The __𝐬𝐭𝐫__ method is intended to provide a human-readable string representation of the object.

While the __𝐫𝐞𝐩𝐫__ method is intended to provide a detailed string representation.

See the example below with the built-in datetime library.

We see that the __𝐫𝐞𝐩𝐫__ method of the datetime class has a detailed and unambiguous output.

import datetime
today = datetime.datetime.now()
 
print("str:", str(today))
 
print("repr: ", repr(today))

# str:   2023-07-02 21:21:00.771969
#repr:  datetime.datetime(2023, 7, 2, 21, 21, 0, 771969)

10.1.15. Neat Way to Merge Dictionaries#

Do you need a clean way to merge two dictionaries in Python?

Use the | operator (available since Python 3.9)

Unlike other methods (like the .update method or unpacking), it offers a neat way to merge dictionaries.

See below for a small example.

dict_1 = {"A": 1, "B": 2}
dict_2 = {"C": 3, "D": 4}

dict_1 | dict_2

10.1.16. Switch Case Statements in Python#

Did you know Python has switch-cases too?

They’re called match-case and it was introduced in Python 3.10+.

It allows you to perform (complex) pattern matching on values and execute code blocks based on the matched patterns.

def match_example(value):
    match value:
        case 1:
            print("Value is 1")

        case 2 | 3:
            print("Value is 2 or 3")
        
        case 4:
            print("Value is 4")
        
        case _:
            print("Value is something else")

match_example(2)

10.1.17. Walrus Operator in Python#

Do you know about the Walrus operator in Python?

The Walrus operator allows you to assign a value to a variable as part of an expression.

# With Walrus Operator
if (name := input("Enter your name: ")):
    print(f"Hello, {name}!")
else:
    print("Hello, anonymous!")
# Without Walrus Operator
name = input("Enter your name: ")
if name:
    print(f"Hello, {name}!")
else:
    print("Hello, anonymous!")

10.1.18. Count Occurrences in an Iterable with Counter#

Don’t count the occurrences of elements in your list manually.

Instead, use collections.Counter in Python.

Counter is used for counting the occurrences of elements in an iterable.

# With Counter
from collections import Counter

fruits = ["apple", "banana", "apple", "orange", "banana", "kiwi"]

fruit_counter = Counter(fruits)

# Output: Counter({'apple': 2, 'banana': 2, 'orange': 1, 'kiwi': 1})
# Without Counter
fruits = ["apple", "banana", "apple", "orange", "banana", "kiwi"]

fruit_counts = {}

for fruit in fruits:
    if fruit in fruit_counts:
        fruit_counts[fruit] += 1
    else:
        fruit_counts[fruit] = 1

10.1.19. Set Default Values for Dictionaries with defaultdict#

Don’t use dictionaries in Python.

Instead use defaultdicts.

defaultdict is similar to dictionaries, but it allows you to set a default value for new keys that haven’t been added yet.

This is useful when you want to perform operations without checking if a key exists.

from collections import defaultdict
      
d = defaultdict(list)

d["Fruits"].append("Kiwi")
d["Cars"].append("Mercedes")
  
print(d["Fruits"])
print(d["Cars"])
print(d["Animals"])

'''
Output:
['Kiwi']
['Mercedes']
[]
'''

10.1.20. More Structured Tuples with namedtuple#

You don’t know about namedtuple?

namedtuple are similar to regular tuples, but have named fields.

This makes them more self-documenting and easier to work with.

from collections import namedtuple
 
Point = namedtuple('Point', ['x', 'y'])
p = Point(x=3.2, y=1.0)
print(p.x, p.y)

10.1.21. Mutable Default Values for Function Arguments#

One big mistake in Python:

Using mutable default values for function arguments.

When using mutable objects like a list as a default value, you have to be careful.

See the example below, where the default list is shared among all calls to the function.

To fix this, set the default value to None and create a new list inside the function if no list is provided.

# Dangerous behaviour:
def increment_numbers(numbers=[]):
    for i in range(3):
        numbers.append(i)
    print(f"Numbers: {numbers}")

increment_numbers()  # Output: Numbers: [0, 1, 2]
increment_numbers()  # Output: Numbers: [0, 1, 2, 0, 1, 2]


# Better:
def increment_numbers(numbers=None):
    if numbers is None:
        numbers = []
    for i in range(3):
        numbers.append(i)
    print(f"Numbers: {numbers}")

increment_numbers()  # Output: Numbers: [0, 1, 2]
increment_numbers()  # Output: Numbers: [0, 1, 2]

10.1.22. Optimize Your Python Objects with __slots__#

Optimize your Python classes with this trick.

When creating objects, a dynamic dictionary is created too to store instance attributes.

But, what if you know all of your attributes beforehand?

With __𝐬𝐥𝐨𝐭𝐬__, you can specify which attributes your object instances will have.

This saves space in memory and prevents users from creating new attributes at runtime.

See below how setting a new instance attribute will throw an error.

class Point:
    __slots__ = ['x', 'y']

    def __init__(self, x, y):
        self.x = x
        self.y = y

point = Point(2,4)
point.z = 5 # Throws an error

10.1.23. Modify Print Statements#

When printing multiple elements in Python, it starts a new line by default.

But, you can change this default to whatever you like by setting the end parameter in print().

See below where we change the end parameter to space.

sports = ["football", "basketball", "volleyball", "tennis", "handball"]

for sport in sports:
    print(sport, end=" ")

10.1.24. Type Variables in Python 3.12#

One cool feature in Python 3.12:

The support for Type Variables.

You can use them to parametrize generic classes and functions.

See below for a small example where our generic class is parametrized by T which we indicate with [T].

class Stack[T]:
    def __init__(self) -> None:
        self.items: List[T] = []

    def push(self, item: T) -> None:
        self.items.append(item)

10.1.25. Enumerations in Python#

To make your Python Code cleaner, use Enums.

Enums (or enumerations) are a way to represent a set of named values as symbolic names.

It provides a way to work with meaningful names instead of raw integers or weird string constants.

Python supports enums with its standard library.

# Option 1
from enum import Enum, auto

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3
    
print(Color.RED.value)  # Output: 1

# Option 2: auto() assigns unique values starting by 1
class Status(Enum):
    PENDING = auto()
    APPROVED = auto()
    REJECTED = auto()
    
print(Status.PENDING.value) # Output: 1

10.1.26. Never use eval#

One big mistake in Python:

Using eval in your code.

eval, like the name suggests, evaluates a Python expression.

The big problem: One could inject malicious code which will be evaluated.

def calculate(expression):
    return eval(expression)
    
print(calculate("5 + 5"))

# DON'T RUN THIS LINE OF CODE
print(calculate("__import__('os').system('rm -rf /')"))

10.1.27. Never use import *#

One nooby Python mistake:

Running import *.

You will only pollute your namespace, leading to potential naming conflicts.

Your code also becomes less readable and maintainable when it’s not clear where each name is coming from.

Remember “Explicit is better than implicit”.

# Ugly
from module import *

# Better
from module import Class1, Class2, Class3

10.1.28. Control What Gets Imported#

How to control what gets imported when you use from module import * in Python?

__all__ lets you specify which symbols should be imported when you run from module import *.

By defining __all__ in your module, you can explicitly control what gets exposed.

# mymodule.py
__all__ = ['my_function', 'MyClass']

def my_function():
    pass

class MyClass:
    pass

def internal_helper():
    pass

# main.py
from mymodule import *

my_function()
internal_helper() # Error

10.1.29. Make Fields Keyword-Only in dataclasses#

If you want to have keyword-only fields in Python, use the kw_only option from dataclasses.

This will throw an error whenever you set parameters without the keyword.

from dataclasses import dataclass
@dataclass(kw_only=True)
class Example:
    a: int
    b: int

example = Example(a=1, b=2)
example = Example(1,2) # Error