Skip to content

Great for - 1. Django Concepts - Flask

Python Syntax Breakdown

Still under heavy construction, this is my favorite language so it will have more information than the others. For a universal approach for static programmers Java is used often for examples.

Basic Structure

# This is a comment
print("Hello, World!")  # Basic output

"""
This is a multi-line comment
or docstring
"""

Variables and Data Types

# Variables (no type declaration needed)
name = "Alice"      # String
age = 30            # Integer
height = 5.7        # Float
is_student = True   # Boolean

# Multiple assignment
x, y, z = 1, 2, 3

# Complex data types
my_list = [1, 2, 3, "four"]                  # List (mutable)
my_tuple = (1, 2, 3, "four")                 # Tuple (immutable)
my_dict = {"name": "Alice", "age": 30}       # Dictionary
my_set = {1, 2, 3, 4}                        # Set (unique values)

Operators

# Arithmetic operators
sum_result = 10 + 5
difference = 10 - 5
product = 10 * 5
quotient = 10 / 5    # Returns float: 2.0
int_quotient = 10 // 5  # Integer division: 2
remainder = 10 % 3   # Modulus: 1
power = 2 ** 3       # Exponentiation: 8

# Comparison operators
equals = (x == y)
not_equals = (x != y)
greater_than = (x > y)
less_or_equal = (x <= y)

# Logical operators
and_result = (x > 0 and y > 0)
or_result = (x > 0 or y > 0)
not_result = not x > 0

# Membership operators
contains = 1 in my_list
not_contains = 5 not in my_list

Control Flow

Conditionals

# If statement
if age < 18:
    print("Minor")
elif age < 65:
    print("Adult")
else:
    print("Senior")

# Ternary operator (conditional expression)
status = "Adult" if age >= 18 else "Minor"

# Match statement (Python 3.10+)
match day:
    case "Monday":
        print("Start of week")
    case "Friday":
        print("Almost weekend")
    case _:  # Default case
        print("Some other day")

Loops

# For loop
for i in range(5):  # 0, 1, 2, 3, 4
    print(i)

# Looping through collections
for item in my_list:
    print(item)

for key, value in my_dict.items():
    print(f"{key}: {value}")

# List comprehension
squares = [x**2 for x in range(10)]

# While loop
count = 0
while count < 5:
    print(count)
    count += 1

# Loop control
for i in range(10):
    if i == 3:
        continue  # Skip this iteration
    if i == 7:
        break  # Exit the loop

Functions

# Basic function
def greet(name):
    return f"Hello, {name}!"

# Function with default parameter
def greet_with_title(name, title="Mr."):
    return f"Hello, {title} {name}!"

# Arbitrary arguments
def sum_all(*args):
    return sum(args)

# Keyword arguments
def build_profile(**kwargs):
    return kwargs

# Lambda functions (anonymous)
square = lambda x: x**2

Classes and Objects

# Class definition
class Person:
    # Class variable
    species = "Homo sapiens"

    # Constructor
    def __init__(self, name, age):
        # Instance variables
        self.name = name
        self.age = age

    # Method
    def greet(self):
        return f"Hello, my name is {self.name}"

    # Static method
    @staticmethod
    def is_adult(age):
        return age >= 18

    # Class method
    @classmethod
    def create_anonymous(cls):
        return cls("Anonymous", 0)

# Creating an object
person = Person("Alice", 30)
greeting = person.greet()

# Inheritance
class Student(Person):
    def __init__(self, name, age, student_id):
        super().__init__(name, age)
        self.student_id = student_id
class Car:
    engine_cls = Engine
    def __init__(self):
        self.engine = self.engine_cls()  # Has-A Engine
    def start(self):
        print(
            f"Starting {self.engine.__class__.__name__} for "
            f"{self.__class__.__name__}... Wroom, wroom!"
        )
        self.engine.start()
    def stop(self):
        self.engine.stop()

class RaceCar(Car):  # Is-A Car
    engine_cls = V8Engine
class CityCar(Car):  # Is-A Car
    engine_cls = ElectricEngine
class F1Car(RaceCar):  # Is-A RaceCar and also Is-A Car
    pass  # engine_cls same as parent

car = Car()
racecar = RaceCar()
citycar = CityCar()
f1car = F1Car()
cars = [car, racecar, citycar, f1car]
for car in cars:
    car.start()

Sharing info to base

# oop/super.explicit.py
class Book:
    def __init__(self, title, publisher, pages):
        self.title = title
        self.publisher = publisher
        self.pages = pages

class Ebook(Book):
    def __init__(self, title, publisher, pages, format_):
        Book.__init__(self, title, publisher, pages)
        self.format_ = format_
ebook = Ebook(
    "Learn Python Programming", "Packt Publishing", 500, "PDF"
)
print(ebook.title)  # Learn Python Programming
print(ebook.publisher)  # Packt Publishing
print(ebook.pages)  # 500
print(ebook.format_)  # PDF

Class methods

Class methods are slightly different from static methods in that, like instance methods, they also receive a special first argument. In their case, it is the class object itself, rather than the instance. A very common use case for class methods is to provide factory capability to a class, which means having alternative ways to create instances of the class.

# oop/class.methods.factory.py
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    @classmethod
    def from_tuple(cls, coords):  # cls is Point
        return cls(*coords)
    @classmethod
    def from_point(cls, point):  # cls is Point
        return cls(point.x, point.y)

p = Point.from_tuple((3, 7))
print(p.x, p.y)  # 3 7
q = Point.from_point(p)
print(q.x, q.y)  # 3 7

Private methods "mangling" In Python, there is no such thing. Everything is public; therefore, we rely on conventions and, for privacy, on a mechanism called name mangling.

class MyClass:
    def __init__(self):
        self.__private_var = 42  # Private variable

    def get_private_var(self):
        return self.__private_var

    def set_private_var(self, value):
        if value >= 0:
            self.__private_var = value
        else:
            print("Invalid value")

# Create an instance of MyClass
obj = MyClass()

# Access the private variable via getter
print(obj.get_private_var())  # Output: 42

# Modify the private variable via setter
obj.set_private_var(100)
print(obj.get_private_var())  # Output: 100

# Attempt to access the private variable directly (will raise an AttributeError)
# print(obj.__private_var)  # Uncommenting this will raise an error

# Accessing the mangled variable name (not recommended)
print(obj._MyClass__private_var)  # Output: 100

use for getters and setters Property decorator

# oop/property.py

class PersonPythonic:
    def __init__(self, age):
        self._age = age
    @property
    def age(self):
        return self._age
    @age.setter
    def age(self, age):
        if 18 <= age <= 99:
            self._age = age
        else:
            raise ValueError("Age must be within [18, 99]")
person = PersonPythonic(39)
print(person.age)  # 39 - Notice we access as data attribute
person.age = 42  # Notice we access as data attribute
print(person.age)  # 42
person.age = 100  # ValueError: Age must be within [18, 99]

Exception Handling

try:
    result = 10 / 0  # Will cause ZeroDivisionError
except ZeroDivisionError:
    print("Cannot divide by zero")
except Exception as e:
    print(f"Some other error occurred: {e}")
else:
    print("No exception occurred")
finally:
    print("This always executes")

# Raising exceptions
if age < 0:
    raise ValueError("Age cannot be negative")

# Custom exceptions
class CustomError(Exception):
    pass

Modules and Imports

# Importing a module
import math
result = math.sqrt(16)

# Importing specific items
from math import sqrt, pi
result = sqrt(16)

# Import with alias
import numpy as np
arr = np.array([1, 2, 3])

# Import all (not recommended)
from math import *

File Operations

# Reading a file
with open("file.txt", "r") as file:
    content = file.read()

# Writing to a file
with open("output.txt", "w") as file:
    file.write("Hello, World!")

# Working with JSON
import json
data = {"name": "Alice", "age": 30}
json_str = json.dumps(data)
parsed_data = json.loads(json_str)

List/Dictionary Comprehensions

#Advanced Comprehensions

# List comprehension
even_numbers = [x for x in range(10) if x % 2 == 0]

# Dictionary comprehension
squares_dict = {x: x**2 for x in range(5)}

# Set comprehension
unique_letters = {letter for letter in "mississippi"}

Context Managers

# Using with statement for resource management
with open("file.txt") as file:
    data = file.read()
# File is automatically closed after this block
Custom context manager:

from contextlib import contextmanager

@contextmanager
def my_manager():
    print("Start")
    yield
    print("End")

with my_manager():
    print("Inside block")

Decorators

# Function decorator
def timer(func):
    def wrapper(*args, **kwargs):
        import time
        start = time.time()
        result = func(*args, **kwargs)
        print(f"Function took {time.time() - start} seconds")
        return result
    return wrapper

@timer
def slow_function():
    import time
    time.sleep(1)
  • Modify function behavior dynamically.
def decorator(func):
    def wrapper(*args, **kwargs):
        print("Before function call")
        result = func(*args, **kwargs)
        print("After function call")
        return result
    return wrapper

@decorator
def say_hello():
    print("Hello!")

say_hello()
# decorators/time.measure.arguments.py
from time import sleep, time

def f(sleep_time=0.1):
    sleep(sleep_time)

def measure(func, *args, **kwargs):
    t = time()
    func(*args, **kwargs)
    print(func.__name__, "took:", time() - t)

measure(f, sleep_time=0.3)  # f took: 0.30092811584472656
measure(f, 0.2)  # f took: 0.20505475997924805

Advanced Approach

# decorators/time.measure.deco1.py
from time import sleep, time
def f(sleep_time=0.1):
    sleep(sleep_time)

def measure(func):
    def wrapper(*args, **kwargs):
        t = time()
        func(*args, **kwargs)
        print(func.__name__, "took:", time() - t)
    return wrapper

f = measure(f)  # decoration point
f(0.2)  # f took: 0.20128178596496582
f(sleep_time=0.3)  # f took: 0.30509519577026367
print(f.__name__)  # wrapper  <- ouch!

We don’t want to lose the original function’s name and docstring when we decorate with a wrapper, notice how the name changed to wrapper, it should have been measure

# decorators/time.measure.deco2.py
from time import sleep, time
from functools import wraps
def measure(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        t = time()
        func(*args, **kwargs)
        print(func.__name__, "took:", time() - t)
    return wrapper
@measure
def f(sleep_time=0.1):
    """I'm a cat. I love to sleep!"""
    sleep(sleep_time)
f(sleep_time=0.3)  # f took: 0.30042004585266113
print(f.__name__)  # f
print(f.__doc__ )  # I'm a cat. I love to sleep!

A decorator factory

Some decorators can take arguments. This technique is generally used to produce another decorator (in which case, the object could be called a decorator factory). Let us look at the syntax, and then we will see an example of it:

# decorators/syntax.py
def func(arg1, arg2, ...):
    pass
func = decoarg(arg_a, arg_b)(func)
# is equivalent to the following:
@decoarg(arg_a, arg_b)
def func(arg1, arg2, ...):
    pass

First, decoarg() is called with the given arguments, and then its return value (the actual decorator) is called with func().

Cached_property Decorator

# oop/cached.property.py
from functools import cached_property
class CachedPropertyManager:
    @cached_property
    def client(self):
        return Client()
    def perform_query(self, **kwargs):
        return self.client.query(**kwargs)
manager = CachedPropertyManager()
manager.perform_query(object_id=42)
manager.perform_query(name_ilike="%Python%")
del manager.client  # This causes a new Client on next call
manager.perform_query(age_gte=18)


Type Hints (Python 3.5+)

def calculate_area(radius: float) -> float:
    """Calculate the area of a circle."""
    import math
    return math.pi * radius ** 2

# With complex types
from typing import List, Dict, Optional

def process_users(users: List[Dict[str, str]]) -> Optional[str]:
    if not users:
        return None
    return users[0]["name"]

Since you already know the basics, let’s break down advanced Python syntax into key concepts with examples:

Advanced Python

Unpacking & Extended Unpacking (*, **)**

  • Extract elements from iterables or dictionaries dynamically.
a, *b, c = [1, 2, 3, 4, 5]
print(a, b, c)  # Output: 1 [2, 3, 4] 5

For dictionaries:

d1 = {'x': 1, 'y': 2}
d2 = {'z': 3, **d1}
print(d2)  # Output: {'z': 3, 'x': 1, 'y': 2}

Walrus Operator (:=)

  • Assign and use a value in an expression.
if (n := len([1, 2, 3])) > 2:
    print(n)  # Output: 3

Lambda with Sorting & Custom Key Functions

  • One-liners for sorting or mapping.
data = [{'name': 'John', 'age': 30}, {'name': 'Jane', 'age': 25}]
data.sort(key=lambda x: x['age'])  # Sorts by age
print(data)

Generator Expressions & Yield

  • Efficient iteration without storing all data in memory.
gen = (x**2 for x in range(5))  # Generator expression
print(next(gen))  # Output: 0
print(next(gen))  # Output: 1

def countdown(n):
    while n > 0:
        yield n
        n -= 1

cd = countdown(3)
print(next(cd))  # Output: 3
print(next(cd))  # Output: 2

Metaclasses (Class of a Class)

  • Modify class behavior dynamically.
class Meta(type):
    def __new__(cls, name, bases, dct):
        dct['created_at'] = '2025'
        return super().__new__(cls, name, bases, dct)

class MyClass(metaclass=Meta):
    pass

print(MyClass.created_at)  # Output: 2025

Coroutines & asyncio (Async Programming)

  • Handle multiple tasks concurrently.
import asyncio

async def fetch_data():
    await asyncio.sleep(2)
    return "Data fetched"

async def main():
    result = await fetch_data()
    print(result)

asyncio.run(main())

Pattern Matching (match-case)

  • Similar to switch-case in other languages.
def process(value):
    match value:
        case 1:
            return "One"
        case 2:
            return "Two"
        case _:
            return "Something else"

print(process(1))  # Output: One

Function Overloading with singledispatch

  • Allows type-specific implementations.
from functools import singledispatch

@singledispatch
def process(val):
    print("Default:", val)

@process.register
def _(val: int):
    print("Integer:", val)

@process.register
def _(val: list):
    print("List:", val)

process(42)    # Integer: 42
process([1, 2, 3])  # List: [1, 2, 3]
process("Hello")    # Default: Hello

Iterations

Permutations

If a set has N elements, then the number of permutations of them is N! (N factorial). For example, the string ABC has 3! = 3 * 2 * 1 = 6 permutations. Let us see this in Python:

# permutations.py
from itertools import permutations
print(list(permutations("ABC")))
result = array of : ABCABCACBBACBCACAB, and CBA.


Infinite Iterators

Infinite iterators allow you to use a for loop as an infinite loop, iterating over a sequence that never ends:

# infinite.py
from itertools import count
for n in count(5, 3):
    if n > 20:
        break
    print(n, end=", ") # instead of newline, comma and space


$ python infinite.py
5, 8, 11, 14, 17, 20,

Iterators terminating on the shortest input sequence

compress(). This iterator takes a sequence of data and a sequence of selectors, yielding only those values from the data sequence that correspond to True values in the selectors sequence.

For example, compress("ABC", (1, 0, 1)) would give back "A" and "C" because they correspond to 1. Let us see a simple example:

# compress.py
from itertools import compress
data = range(10)
even_selector = [1, 0] * 10
odd_selector = [0, 1] * 10
even_numbers = list(compress(data, even_selector))
odd_numbers = list(compress(data, odd_selector))
print(odd_selector)
print(list(data))
print(even_numbers)
print(odd_numbers)

output

$ python compress.py 
[0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1] 
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 
[0, 2, 4, 6, 8] 
[1, 3, 5, 7, 9]


Parameters

Variable Positional Parameters

Sometimes you may prefer not to specify the exact number of positional parameters to a function; Python provides you with the ability to do this by using variable positional parameters

# parameters.variable.positional.py
def minimum(*n):
    # print(type(n))  # n is a tuple
    if n:  # explained after the code
        mn = n[0]
        for value in n[1:]:
            if value < mn:
                mn = value
        print(mn)
minimum(1, 3, -7, 9)  # n = (1, 3, -7, 9) - prints: -7
minimum()  # n = () - prints: nothing

Variable keyword parameters

# parameters.variable.keyword.py
def func(**kwargs):
    print(kwargs)
func(a=1, b=42)  # prints {'a': 1, 'b': 42}
func()  # prints {}
func(a=1, b=46, c=99)  # prints {'a': 1, 'b': 46, 'c': 99}

Function that connects to a database: we want to connect to a default database by simply calling this function with no parameters.

Realistic Example

# parameters.variable.db.py
def connect(**options):
    conn_params = {
        "host": options.get("host", "127.0.0.1"),
        "port": options.get("port", 5432),
        "user": options.get("user", ""),
        "pwd": options.get("pwd", ""),
    }
    print(conn_params)
    # we then connect to the db (commented out)
    # db.connect(**conn_params)
connect()
connect(host="127.0.0.42", port=5433)
connect(port=5431, user="fab", pwd="gandalf")

Result

$ python parameters.variable.db.py
{'host': '127.0.0.1', 'port': 5432, 'user': '', 'pwd': ''}
{'host': '127.0.0.42', 'port': 5433, 'user': '', 'pwd': ''}
{'host': '127.0.0.1', 'port': 5431, 'user': 'fab', 'pwd': 'gandalf'}


Positional-only parameters

There is a new function parameter syntax, /, indicating that a set of the function parameters must be specified positionally and cannot be passed as keyword arguments. Let us see a simple example:

# parameters.positional.only.py
def func(a, b, /, c):
    print(a, b, c)
func(1, 2, 3)  # prints: 1 2 3
func(1, 2, c=3)  # prints 1 2 3
func(1, b=2, c=3) # error

can still be optional

# parameters.positional.only.optional.py
def func(a, b=2, /):
    print(a, b)
func(4, 5)  # prints 4 5
func(3)  # prints 3 2

def func_name(name, /, **kwargs):
    print(name)
    print(kwargs)
func_name("Positional-only name", name="Name in **kwargs")
# Prints:
# Positional-only name
# {'name': 'Name in **kwargs'}

Keyword-only parameters

# parameters.keyword.only.py
def kwo(*a, c):
    print(a, c)
kwo(1, 2, 3, c=7)  # prints: (1, 2, 3) 7
kwo(c=4)  # prints: () 4
# kwo(1, 2)  # breaks, invalid syntax, with the following error
# TypeError: kwo() missing 1 required keyword-only argument: 'c'

def kwo2(a, b=42, *, c):
    print(a, b, c)
kwo2(3, b=7, c=99)  # prints: 3 7 99
kwo2(3, c=13)  # prints: 3 42 13
# kwo2(3, 23)  # breaks, invalid syntax, with the following error
# TypeError: kwo2() missing 1 required keyword-only argument: 'c'

Combining input parameters

Restrictions when ordering: - Positional-only parameters come first, followed by a /. - Normal parameters go after any positional-only parameters. - Variable positional parameters go after normal parameters. - Keyword-only parameters go after variable positional parameters. - Variable keyword parameters always go last. - For positional-only and normal parameters, any required parameters must be defined before any optional parameters. This means that if you have an optional positional-only parameter, all your normal parameters must be optional too. This rule does not affect keyword-only parameters.

samples

# parameters.all.py
def func(a, b, c=7, *args, **kwargs):
    print("a, b, c:", a, b, c)
    print("args:", args)
    print("kwargs:", kwargs)
func(1, 2, 3, 5, 7, 9, A="a", B="b")

Note the order of the parameters in the function definition. The execution of this yields the following:

$ python parameters.all.py
a, b, c: 1 2 3
args: (5, 7, 9)
kwargs: {'A': 'a', 'B': 'b'}

Key word sample

# parameters.all.pkwonly.py
def allparams(a, /, b, c=42, *args, d=256, e, **kwargs):
    print("a, b, c:", a, b, c)
    print("d, e:", d, e)
    print("args:", args)
    print("kwargs:", kwargs)
allparams(1, 2, 3, 4, 5, 6, e=7, f=9, g=10)

result

$ python parameters.all.pkwonly.py
a, b, c: 1 2 3
d, e: 256 7
args: (4, 5, 6)
kwargs: {'f': 9, 'g': 10}

Signature examples

def func_name(positional_only_parameters, /,
    positional_or_keyword_parameters, *,
    keyword_only_parameters):
First, we have positional-only, then positional or keyword parameters, and finally keyword-only ones.

Some other valid signatures are presented below:

def func_name(p1, p2, /, p_or_kw, *, kw):
def func_name(p1, p2=None, /, p_or_kw=None, *, kw):
def func_name(p1, p2=None, /, *, kw):
def func_name(p1, p2=None, /):
def func_name(p1, p2, /, p_or_kw):
def func_name(p1, p2, /):

Invalid signatures

def func_name(p1, p2=None, /, p_or_kw, *, kw):
def func_name(p1=None, p2, /, p_or_kw=None, *, kw):
def func_name(p1=None, p2, /):


reduce() in Python

reduce() is a function from the functools module that applies a given function cumulatively to the elements of an iterable, reducing it to a single value.

Syntax:

from functools import reduce
reduce(function, iterable[, initializer])
  • function: A function that takes two arguments.
  • iterable: The sequence to process.
  • initializer (optional): A starting value.

Example:

from functools import reduce

numbers = [1, 2, 3, 4, 5]
result = reduce(lambda x, y: x + y, numbers)
print(result)  # Output: 15

Explanation:
reduce() applies lambda x, y: x + y like this:
(((1 + 2) + 3) + 4) + 515


mul() in Python

mul() (short for multiply) is a function from the operator module that performs multiplication on two numbers.

Syntax:

from operator import mul
mul(a, b)
  • a and b are the two numbers to multiply.

Example:

from operator import mul

result = mul(3, 4)
print(result)  # Output: 12

Using mul() with reduce()

If you want to multiply all numbers in a list, you can use reduce() with mul():

from functools import reduce
from operator import mul

numbers = [1, 2, 3, 4, 5]
product = reduce(mul, numbers)
print(product)  # Output: 120

This computes 1 * 2 * 3 * 4 * 5 = 120.


Recursive method

Recursion is a programming technique where a function calls itself to solve smaller instances of the same problem. A recursive function typically has two key parts:

  1. Base Case – The condition where the function stops calling itself.
  2. Recursive Case – The part where the function calls itself with a modified argument.
def factorial(n):
    if n == 1:  # Base case
        return 1
    return n * factorial(n - 1)  # Recursive step

# Example usage
print(factorial(5))  # Output: 120

Good for problems with natural recursive structures, like: - Tree traversal - Graph traversal (DFS) - Divide and conquer algorithms (Merge Sort, Quick Sort)

Example: Factorial Using Recursion

Factorial of n (n!) is n * (n-1) * (n-2) ... * 1. This can be written recursively as:

def factorial(n):
    if n == 1:  # Base case
        return 1
    return n * factorial(n - 1)  # Recursive case

print(factorial(5))  # Output: 120

How it works:

factorial(5)  5 * factorial(4)
factorial(4)  4 * factorial(3)
factorial(3)  3 * factorial(2)
factorial(2)  2 * factorial(1)
factorial(1)  1 (Base case)

result

1  2  6  24  120


Anonymous functions

One last type of function that we want to talk about is anonymous functions. These functions, which are called lambdas in Python, are usually used when a fully-fledged function with its own name would be overkill, and all we want is a quick, simple one-liner.

Here’s a simple example of a lambda function in Python:

# Lambda function to add two numbers
add = lambda x, y: x + y

# Using the lambda function
result = add(3, 5)
print(result)  # Output: 8

Explanation:

  • lambda x, y: x + y is an anonymous function that takes two arguments (x and y) and returns their sum.
  • The function is assigned to the variable add, so it can be used like a normal function.

More Lambda Examples

Squaring a number:

square = lambda x: x ** 2
print(square(4))  # Output: 16

Checking if a number is even:

is_even = lambda x: x % 2 == 0
print(is_even(10))  # Output: True
print(is_even(7))   # Output: False

Using lambda inside map():

numbers = [1, 2, 3, 4]
squared_numbers = list(map(lambda x: x ** 2, numbers))
print(squared_numbers)  # Output: [1, 4, 9, 16]

Using lambda inside filter():

numbers = [1, 2, 3, 4, 5, 6]
even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
print(even_numbers)  # Output: [2, 4, 6]

---

## Function attributes

Every function is a fully fledged object and, as such, it has several attributes. Some of them are special and can be used in an introspective way to inspect the function object at runtime. 

The following script is an example that shows a few of them and how to display their value for an example function:

```python

# func.attributes.py
def multiplication(a, b=1):
    """Return a multiplied by b."""
    return a * b
if __name__ == "__main__":
    special_attributes = [
        "__doc__",
        "__name__",
        "__qualname__",
        "__module__",
        "__defaults__",
        "__code__",
        "__globals__",
        "__dict__",
        "__closure__",
        "__annotations__",
        "__kwdefaults__",
    ]
    for attribute in special_attributes:
        print(attribute, "->", getattr(multiplication, attribute))

Output:

$ python func.attributes.py
__doc__ -> Return a multiplied by b.
__name__ -> multiplication
__qualname__ -> multiplication
__module__ -> __main__
__defaults__ -> (1,)
__code__ -> <code object multiplication at 0x102ce1550,
             file "func.attributes.py", line 2>
__globals__ -> {... omitted ...}
__dict__ -> {}
__closure__ -> None
__annotations__ -> {}
__kwdefaults__ -> None
We have omitted the value of the __globals__ attribute, as it was too big.

NB You can use the built-in dir() function to get a list of all the attributes of any object.

Notice this from the previous code?

if __name__ == "__main__":

This line makes sure that whatever follows is only executed when the module is run directly. When you run a Python script, Python sets the __name__ variable to "__main__" in that script.

Conversely, when you import a Python script as a module into another script, the __name__ variable is set to the name of the script/module being imported.


Python Built-in functions

Python comes with a lot of built-in functions. They are available anywhere, and you can get a list of them by inspecting the builtins module with dir(__builtins__), or by going to the official Python documentation

dir(__builtins__)
few examples anybinbooldivmodfilterfloatgetattridintlenlistminprintsettupletype, help and zip

The help() built-in function, which is intended for interactive use, creates a documentation page for an object using its docstring.


Importing objects

# imports.py
from datetime import datetime, timezone  # two imports, same line
from unittest.mock import patch  # single import
import pytest  # third party library
from core.models import (  # multiline import
    Exam,
    Exercise,
    Solution,
)
from mymodule import myfunc as better_named_func # identifier

Sample Imagine that we have defined a couple of functions, square(n) and cube(n), in a module, funcdef.py, which is in the util folder. We want to use them in a couple of modules that are at the same level as the util folder, called func_import.py and func_from.py. Showing the tree structure of that project produces something like this:

├── func_from.py
├── func_import.py
├── util
│   ├── __init__.py
│   └── funcdef.py

Before we show you the code of each module, please remember that in order to tell Python that it is actually a package, we need to put an __init__.py module in it.

# util/funcdef.py
def square(n):
    return n**2
def cube(n):
    return n**3

# func_import.py
import util.funcdef
print(util.funcdef.square(10))
print(util.funcdef.cube(10))

# func_from.py
from util.funcdef import square, cube
print(square(10))
print(cube(10))

Map, Zip and Filter

map() Function

map() applies a function to every item in an iterable and returns a map object (which can be converted to a list, tuple, etc.).

Syntax:

map(function, iterable)

Example: Squaring a List

numbers = [1, 2, 3, 4]
squared = list(map(lambda x: x ** 2, numbers))
print(squared)  # Output: [1, 4, 9, 16]

Explanation:

  • lambda x: x ** 2 squares each number.
  • map() applies this to each item in numbers.

To decorate an object means to transform it, either adding extra data to it or putting it into another object. Conversely, to undecorate an object means to revert the decorated object to its original form.

Advanced Map Example:

# decorate.sort.undecorate.py
from pprint import pprint
students = [
    dict(id=0, credits=dict(math=9, physics=6, history=7)),
    dict(id=1, credits=dict(math=6, physics=7, latin=10)),
    dict(id=2, credits=dict(history=8, physics=9, chemistry=10)),
    dict(id=3, credits=dict(math=5, physics=5, geography=7)),
]

def decorate(student):
    # create a 2-tuple (sum of credits, student) from student dict
    return (sum(student["credits"].values()), student)

def undecorate(decorated_student):
    # discard sum of credits, return original student dict
    return decorated_student[1]

print(students[0])
print(decorate(students[0])

students = sorted(map(decorate, students), reverse=True)
students = list(map(undecorate, students))
pprint(students)

Output:

{'id': 0, 'credits': {'math': 9, 'physics': 6, 'history': 7}}
(22, {'id': 0, 'credits': {'math': 9, 'physics': 6, 'history': 7}})

[{'credits': {'chemistry': 10, 'history': 8, 'physics': 9}, 'id': 2},
 {'credits': {'latin': 10, 'math': 6, 'physics': 7}, 'id': 1},
 {'credits': {'history': 7, 'math': 9, 'physics': 6}, 'id': 0},
 {'credits': {'geography': 7, 'math': 5, 'physics': 5}, 'id': 3}]

As you can see, the student objects have indeed been sorted by the sums of their credits.


zip() Function

zip() pairs elements from multiple iterables into tuples.

Syntax:

zip(iterable1, iterable2, ...)

Example: Pairing Two Lists

names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]

paired = list(zip(names, ages))
print(paired)  
# Output: [('Alice', 25), ('Bob', 30), ('Charlie', 35)]

Explanation:

  • Combines corresponding elements from names and ages into tuples.

Example: Unzipping

unzipped_names, unzipped_ages = zip(*paired)
print(unzipped_names)  # Output: ('Alice', 'Bob', 'Charlie')
print(unzipped_ages)   # Output: (25, 30, 35)

Sample:

# zip.strict.txt
>>> students = ["Sophie", "Alex", "Charlie", "Alice"]
>>> grades = ["A", "C", "B"]
>>> dict(zip(students, grades))
{'Sophie': 'A', 'Alex': 'C', 'Charlie': 'B'}

Notice alice was left out The strict keyword-only parameter was added in Python 3.10. If zip() receives strict=True as an argument, it raises an exception if the iterables do not all have the same length:

>>> dict(zip(students, grades, strict=True))

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: zip() argument 2 is shorter than argument 1

Note The itertools module also provides a zip_longest() function. It behaves like zip() but stops only when the longest iterable is exhausted. Shorter iterables are padded with a value that can be specified as an argument, which defaults to None.


filter() Function

filter() removes elements that don’t satisfy a condition (returns True).

Syntax:

filter(function, iterable)

Example: Filtering Even Numbers

numbers = [1, 2, 3, 4, 5, 6]
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens)  # Output: [2, 4, 6]

Explanation:

  • The lambda function x % 2 == 0 keeps only even numbers.

Sample 2:

# filter.txt
>>> test = [2, 5, 8, 0, 0, 1, 0]
>>> list(filter(None, test))
[2, 5, 8, 1]
>>> list(filter(lambda x: x, test))  # equivalent to previous one
[2, 5, 8, 1]
>>> list(filter(lambda x: x > 4, test))  # keep only items > 4
[5, 8]


Key Differences

Function Purpose
map() Transforms each item in an iterable
zip() Combines multiple iterables element-wise
filter() Selects items based on a condition

Advanced Comprehensions

Standard approach

# squares.map.txt
>>> squares = list(map(lambda n: n**2, range(10)))
>>> squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Comprehension:

# squares.comprehension.txt
>>> [n**2 for n in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Sample 2:

# even.squares.py
# using map and filter
sq1 = list(
    map(lambda n: n**2, filter(lambda n: not n % 2, range(10)))
)
# equivalent, but using list comprehensions
sq2 = [n**2 for n in range(10) if not n % 2]
print(sq1, sq1 == sq2)  # prints: [0, 4, 16, 36, 64] True


Nested Comprehensions

Let us see the classical for loop equivalent:

# pairs.for.loop.py
items = "ABCD"
pairs = []
for a in range(len(items)):
    for b in range(a, len(items)):
        pairs.append((items[a], items[b]))

Output:

$ python pairs.for.loop.py
[('A', 'A'), ('A', 'B'), ('A', 'C'), ('A', 'D'), ('B', 'B'), ('B', 'C'), ('B', 'D'), ('C', 'C'), ('C', 'D'), ('D', 'D')]

Comprehension:

# pairs.list.comprehension.py
items = "ABCD"
pairs = [
    (items[a], items[b])    
    for a in range(len(items))
    for b in range(a, len(items))
]


Filtering a comprehension

We can also apply filtering to a comprehension. Let us first do it with filter(), and find all Pythagorean triples whose short sides are numbers smaller than 10.

Level 1:

# pythagorean.triple.py
from math import sqrt
# this will generate all possible pairs
mx = 10
triples = [
    (a, b, sqrt(a**2 + b**2))
    for a in range(1, mx)
    for b in range(a, mx)
]
# this will filter out all non-Pythagorean triples
triples = list(
    filter(lambda triple: triple[2].is_integer(), triples)
)
print(triples)  # prints: [(3, 4, 5.0), (6, 8, 10.0)]

This is good, but we do not like the fact that the triple has two integer numbers and a float—they are all supposed to be integers. We can use map() to fix this:

Level 2:

# pythagorean.triple.int.py
from math import sqrt
mx = 10
triples = [
    (a, b, sqrt(a**2 + b**2))
    for a in range(1, mx)
    for b in range(a, mx)
]
triples = filter(lambda triple: triple[2].is_integer(), triples)
# this will make the third number in the tuples integer
triples = list(
    map(lambda triple: triple[:2] + (int(triple[2]),), triples)
)
print(triples)  # prints: [(3, 4, 5), (6, 8, 10)]

Notice the step we added. We slice each element in triples, taking only the first two elements. Then, we concatenate the slice with a one-tuple, containing the integer version of that float number that we did not like.

This code is getting quite complicated. We can achieve the same result with a much simpler list comprehension:

Level 3:

# pythagorean.triple.comprehension.py
from math import sqrt
# this step is the same as before
mx = 10
triples = [
    (a, b, sqrt(a**2 + b**2))
    for a in range(1, mx)
    for b in range(a, mx)
]
# here we combine filter and map in one CLEAN list comprehension
triples = [
    (a, b, int(c)) for a, b, c in triples if c.is_integer()
]
print(triples)  # prints: [(3, 4, 5), (6, 8, 10)]

That is cleaner, easier to read, and shorter. There is still room for improvement, though. We are still wasting memory by constructing a list with many triples that we end up discarding. We can fix that by combining the two comprehensions into one:

Level 4:

# pythagorean.triple.walrus.py
from math import sqrt
# this step is the same as before
mx = 10
# We can combine generating and filtering in one comprehension
triples = [
    (a, b, int(c))
    for a in range(1, mx)
    for b in range(a, mx)
    if (c := sqrt(a**2 + b**2)).is_integer()
]
print(triples)  # prints: [(3, 4, 5), (6, 8, 10)]

Now that is elegant. By generating the triples and filtering them in the same list comprehension, we avoid keeping any triple that does not pass the test in memory. Notice that we used an assignment expression to avoid needing to compute the value of sqrt(a**2 + b**2) twice.


Dictionary comprehensions

Dictionary comprehensions work exactly like list comprehensions, but to construct dictionaries. There is only a slight difference in the syntax.

# dictionary.comprehensions.py
from string import ascii_lowercase
lettermap = {c: k for k, c in enumerate(ascii_lowercase, 1)}

# same way
lettermap = dict((c, k) for k, c in enumerate(ascii_lowercase, 1))

Output :

$ python dictionary.comprehensions.py
{'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6, 'g': 7, 'h': 8,
'i': 9, 'j': 10, 'k': 11, 'l': 12, 'm': 13, 'n': 14, 'o': 15,
'p': 16, 'q': 17, 'r': 18, 's': 19, 't': 20, 'u': 21, 'v': 22,
'w': 23, 'x': 24, 'y': 25, 'z': 26}

Dictionaries do not allow duplicate keys, as shown in the following example:

# dictionary.comprehensions.duplicates.py
word = "Hello"
swaps = {c: c.swapcase() for c in word}
print(swaps)  # prints: {'H': 'h', 'e': 'E', 'l': 'L', 'o': 'O'}

Set Comprehensions

basically removes duplicates

Example: Filtering Even Numbers

numbers = [1, 2, 3, 4, 5, 6, 2, 4, 6]
evens = {x for x in numbers if x % 2 == 0}
print(evens)  
# Output: {2, 4, 6}  (Duplicates removed)

Explanation:

  • {x for x in numbers if x % 2 == 0} keeps only even numbers.
  • Since it's a set, duplicates are removed automatically.

Example: Extract Unique Letters

word = "banana"
unique_letters = {letter for letter in word}
print(unique_letters)  
# Output: {'b', 'a', 'n'}

Explanation:

  • {letter for letter in word} extracts unique characters from "banana".

Custom iterators

class MyRange:
    def __init__(self, start, end):
        self.start = start
        self.end = end
        self.current = start

    def __iter__(self):
        # The __iter__ method returns the iterator object itself.
        return self

    def __next__(self):
        # The __next__ method returns the next value.
        if self.current >= self.end:
            raise StopIteration  # StopIteration signals the end of iteration
        else:
            self.current += 1
            return self.current - 1

# Create an instance of MyRange
my_range = MyRange(1, 5)

# Iterate through the MyRange object
for num in my_range:
    print(num)

Exception Groups

When working with large collections of data, it can be inconvenient to immediately stop and raise an exception when an error occurs. It is often better to process all the data and report on all errors that occurred at the end. This allows the user to deal with all the errors at once, rather than having to rerun the process multiple times, fixing errors one by one.

# exceptions/groups/util.py
def validate_age(age):
    if not isinstance(age, int):
        raise TypeError(f"Not an integer: {age}")
    if age < 0:
        raise ValueError(f"Negative age: {age}")

def validate_ages(ages):
    errors = []
    for age in ages:
        try:
            validate_age(age)
        except Exception as e:
            errors.append(e)
    if errors:
        raise ExceptionGroup("Validation errors", errors)
# exceptions/groups/exc.group.txt
>>> from util import validate_ages
>>> validate_ages([24, -5, "ninety", 30, None])
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 1, in <module>
  |   File "exceptions/groups/util.py", line 20, in validate_ages
  |     raise ExceptionGroup("Validation errors", errors)
  | ExceptionGroup: Validation errors (3 sub-exceptions)

  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "exceptions/groups/util.py", line 15, in validate_ages
    |     validate_age(age)
    |   File "exceptions/groups/util.py", line 8, in validate_age
    |     raise ValueError(f"Negative age: {age}")
    | ValueError: Negative age: -5
    +---------------- 2 ----------------
    | Traceback (most recent call last):
    |   File "exceptions/groups/util.py", line 15, in validate_ages
    |     validate_age(age)
    |   File "exceptions/groups/util.py", line 6, in validate_age
    |     raise TypeError(f"Not an integer: {age}")
    | TypeError: Not an integer: ninety
    +---------------- 3 ----------------
    | Traceback (most recent call last):
    |   File "exceptions/groups/util.py", line 15, in validate_ages
    |     validate_age(age)
    |   File "exceptions/groups/util.py", line 6, in validate_age
    |     raise TypeError(f"Not an integer: {age}")
    | TypeError: Not an integer: None
    +------------------------------------

Exceptions for stopping loops

class StopLoopException(Exception):
    pass

try:
    for i in range(10):  # Looping from 0 to 9
        if i == 5:  # Condition to raise the exception
            raise StopLoopException(i)  # Raise exception with i as argument
        print(i)  # Print numbers from 0 to 4
except StopLoopException as e:
    print(f"Loop stopped at: {e.args[0]}")  # Print the value where the loop stopped

Context Managers Deep

Context managers in Python are used to set up and tear down resources, like opening and closing files, acquiring and releasing locks, or managing database connections. They allow you to manage resources in a clean, concise, and reliable way, ensuring that certain actions are performed before and after a block of code runs, even if exceptions occur.

How Context Managers Work

Under the hood, context managers rely on two special methods:

  1. __enter__(): This method is executed when the with block is entered. It sets up the resource and can return any object that is needed for the block of code.
  2. __exit__(): This method is executed when the with block is exited. It handles clean-up actions, such as releasing resources, closing files, or rolling back transactions. If an exception occurs within the with block, __exit__() can handle it or propagate it.
class MyContextManager:
    def __enter__(self):
        print("Entering the context.")
        return self  # This can be any object you want to return and use inside the block.

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("Exiting the context.")
        if exc_type:
            print(f"An error occurred: {exc_val}")
        return True  # Suppress exceptions (optional)

# Using the custom context manager
with MyContextManager() as cm:
    print("Inside the context.")
    # Uncomment the next line to see exception handling in action
    # raise ValueError("Something went wrong!")

output

Entering the context.
Inside the context.
Exiting the context.

Advanced

# context/manager.class.py
ctx_mgr = MyContextManager()
print("About to enter 'with' context")

with ctx_mgr as mgr:
    print("Inside 'with' context")
    print(id(mgr))
    raise Exception("Exception inside 'with' context")
    print("This line will never be reached")

print("After 'with' context")

output

$ python context/manager.class.py
MyContextManager init 140340228792272
About to enter 'with' context
Entering 'with' context
Inside 'with' context
140340228792272
exc_type=<class 'Exception'> exc_val=Exception("Exception inside
'with' context") exc_tb=<traceback object at 0x7fa3817c5340>
Exiting 'with' context
After 'with' context

Class-Based Context Managers

Class-based context managers are implemented using the special methods __enter__() and __exit__(). These methods define how to acquire and release resources.

class DatabaseConnection:
    def __enter__(self):
        print("Opening database connection...")
        self.connection = "Connected to database"  # Simulating a database connection
        return self.connection

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("Closing database connection...")
        # Handle any exceptions that occurred within the block
        if exc_type:
            print(f"An error occurred: {exc_val}")
        return True  # Suppress exceptions if desired

# Using the context manager
with DatabaseConnection() as conn:
    print(conn)  # Use the simulated database connection
    # Uncomment the next line to simulate an exception
    # raise ValueError("Database error")

Output

Opening database connection...
Connected to database
Closing database connection...

Files and Directories

Opening files

# files/open_with.py
with open("fear.txt") as fh:
    for line in fh:
        print(line.strip())

Checking for file and directory existence

If you want to make sure a file or directory exists (or does not), the pathlib module is what you need.

# files/existence.py
from pathlib import Path

p = Path("fear.txt")
path = p.parent.absolute()

print(p.is_file())  # True
print(path)  # /Users/fab/code/lpp4ed/ch08/files
print(path.is_dir())  # True
q = Path("/Users/fab/code/lpp4ed/ch08/files")
print(q.is_dir())  # True

Binary Files

Reading and writing in binary mode

Notice that by opening a file and passing t in the options (or omitting it, as it is the default), we are opening the file in text mode. This means that the content of the file is treated and interpreted as text.

If you wish to write bytes to a file, you can open it in binary mode. This is a common requirement when you handle files that do not just contain raw text, such as images, audio/video, and, in general, any other proprietary format.

To handle files in binary mode, simply specify the b flag when opening them, as in the following example:

# files/read_write_bin.py
with open("example.bin", "wb") as fw:
    fw.write(b"This is binary data...")
with open("example.bin", "rb") as f:
    print(f.read())  # prints: b'This is binary data...'

Write to non existent file

# files/write_not_exists.py
with open("write_x.txt", "x") as fw:  # this succeeds
    fw.write("Writing line 1")
with open("write_x.txt", "x") as fw:  # this fails
    fw.write("Writing line 2")

Custom Json Encoding/Decoding

# json_examples/json_cplx.py
import json
class ComplexEncoder(json.JSONEncoder):
    def default(self, obj):
        print(f"ComplexEncoder.default: {obj=}")
        if isinstance(obj, complex):
            return {
                "_meta": "complex",
                "num": [obj.real, obj.imag],
            }
        return super().default(obj)
data = {
    "an_int": 42,
    "a_float": 3.14159265,
    "a_complex": 3 + 4j,
}
json_data = json.dumps(data, cls=ComplexEncoder)
print(json_data)
def object_hook(obj):
    print(f"object_hook: {obj=}")
    try:
        if obj["_meta"] == "complex":
            return complex(*obj["num"])
    except KeyError:
        return obj
data_out = json.loads(json_data, object_hook=object_hook)
print(data_out)

output:

$ python json_cplx.py
ComplexEncoder.default: obj=(3+4j)
{
    "an_int": 42, "a_float": 3.14159265,
    "a_complex": {"_meta": "complex", "num": [3.0, 4.0]}
}

Sample 2:

# json_examples/json_datetime.py
import json
from datetime import datetime, timedelta, timezone
now = datetime.now()
now_tz = datetime.now(tz=timezone(timedelta(hours=1)))
class DatetimeEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            try:
                off = obj.utcoffset().seconds
            except AttributeError:
                off = None
            return {
                "_meta": "datetime",
                "data": obj.timetuple()[:6] + (obj.microsecond,),
                "utcoffset": off,
            }
        return super().default(obj)
data = {
    "an_int": 42,
    "a_float": 3.14159265,
    "a_datetime": now,
    "a_datetime_tz": now_tz,
}
json_data = json.dumps(data, cls=DatetimeEncoder)
print(json_data)

Output

$ python json_datetime.py
{
    "an_int": 42,
    "a_float": 3.14159265,
    "a_datetime": {
        "_meta": "datetime",
        "data": [2024, 3, 29, 23, 24, 22, 232302],
        "utcoffset": null,
    },
    "a_datetime_tz": {
        "_meta": "datetime",
        "data": [2024, 3, 30, 0, 24, 22, 232316],
        "utcoffset": 3600,
    },
}