Skip to content

Python Aide-mémoire

Langage polyvalent et lisible pour le web, les données, l'IA et l'automatisation.

01

Premiers pas

Hello World & Commentaires

Python utilise # pour les commentaires et les triples guillemets pour les docstrings. La fonction print() prend en charge les paramètres sep et end pour personnaliser le formatage de sortie. Les docstrings servent de documentation accessible via help() et __doc__.

python
# This is a single-line comment
"""
This is a
multi-line comment (docstring)
"""
print("Hello, World!")        # print to stdout
print("A", "B", "C", sep="-") # A-B-C
print("No newline", end="")   # suppress newline

Indentation & Blocs de code

Contrairement à la plupart des langages, Python utilise l'indentation plutôt que les accolades pour définir les blocs de code. La cohérence est essentielle — mélanger des tabulations et des espaces provoque une SyntaxError. PEP 8 recommande 4 espaces par niveau.

python
# Python uses indentation (4 spaces) to define blocks
if True:
    print("inside if")
    if True:
        print("nested block")
print("outside block")

# No braces! Indentation IS the syntax
def func():
    x = 1
    return x + 1

Entrée & Sortie

input() lit depuis stdin sous forme de chaîne — convertissez toujours lorsque vous avez besoin d'un nombre. Utilisez int(), float(), etc. pour la conversion. Les f-strings (Python 3.6+) sont la méthode préférée pour formater les chaînes.

python
# input() always returns a string
name = input("Enter your name: ")
age = int(input("Enter your age: "))  # convert to int

print(f"Hello {name}, you are {age} years old")

# formatted output
print("Pi is approximately {:.2f}".format(3.14159))
print(f"{1000000:,}")  # 1,000,000 with thousands separator

Instructions multiples & Continuation de ligne

Utilisez des points-virgules pour séparer les instructions sur une ligne (rare en Python idiomatique). Les longues lignes peuvent être continuées avec une barre oblique inversée, ou automatiquement à l'intérieur de (), [], {}. Préférez la continuation implicite pour la lisibilité.

python
# multiple statements on one line (discouraged)
a = 1; b = 2; c = 3

# explicit line continuation
total = 1 + 2 + 3 + \
        4 + 5 + 6

# implicit continuation inside brackets
nums = [
    1, 2, 3,
    4, 5, 6
]

result = (1 + 2
          + 3 + 4)

Exécution Python

Les scripts Python s'exécutent avec 'python script.py'. Le REPL permet l'expérimentation interactive. Utilisez toujours python3 sur les systèmes où python pointe vers Python 2. La ligne shebang rend les scripts exécutables sur Unix.

python
# Run a script
# $ python script.py

# Run interactively (REPL)
# $ python
# >>> 2 + 2
# 4

# Shebang line for Unix scripts
#!/usr/bin/env python3

# Check Python version
import sys
print(sys.version)
print(sys.version_info.major)  # 3
02

Variables & Types de données

Variables & Typage dynamique

Python utilise le typage dynamique — les variables peuvent changer de type à l'exécution. Utilisez type() pour vérifier, isinstance() pour vérifier. Python 3.6+ prend en charge les indications de type (name: str = 'Alice') pour le support IDE sans application à l'exécution.

python
# Python is dynamically typed - no declaration needed
name = "Alice"          # str
age = 30                # int
height = 5.7            # float
is_active = True        # bool
items = [1, 2, 3]       # list

# Type checking
print(type(name))       # <class 'str'>
print(isinstance(age, int))  # True

# Multiple assignment
x, y, z = 1, 2, 3
a = b = 0  # chain assignment

Indications de type (Python 3.6+)

Les indications de type améliorent la lisibilité du code et activent l'autocomplétion IDE et l'analyse statique avec mypy. Elles ne sont PAS appliquées à l'exécution — Python reste dynamiquement typé. Utilisez Optional[X] pour les valeurs qui peuvent être None.

python
# Variable annotations
name: str = "Alice"
age: int = 30
scores: list[float] = [90.5, 85.0]

# Function annotations
def greet(name: str, times: int = 1) -> str:
    return (f"Hi {name}! " * times).strip()

# Optional and Union
from typing import Optional, Union
def find(id: int) -> Optional[str]:
    return "Alice" if id == 1 else None

# mypy for static type checking
# $ mypy script.py

Conversion de type

Python possède des fonctions de conversion intégrées : int(), float(), str(), bool(), list(), tuple(), set(), dict(). Les valeurs fausses incluent 0, '', [], {}, None, False. int() tronque vers zéro, tandis que round() utilise l'arrondi du banquier.

python
# String to number
num_str = str(42)          # "42"
num = int("42")            # 42
float_num = float("3.14")  # 3.14

# Number conversions
print(int(3.99))           # 3 (truncates toward zero)
print(int(-3.99))          # -3
print(round(3.14159, 2))   # 3.14

# Boolean conversion
print(bool(0))             # False
print(bool(""))            # False
print(bool([]))            # False
print(bool("anything"))    # True

# Collection conversions
print(list("abc"))         # ['a', 'b', 'c']
print(tuple([1, 2, 3]))    # (1, 2, 3)
print(set([1, 1, 2]))      # {1, 2}

Types numériques

Les entiers Python ont une précision arbitraire (pas de débordement). Les flottants sont des doubles IEEE 754 avec les problèmes de précision habituels. Les nombres complexes sont intégrés. Les booléens sont une sous-classe de int (True==1, False==0). Utilisez des tirets bas dans les littéraux numériques pour la lisibilité.

python
# Integers (arbitrary precision)
big = 10 ** 100  # no overflow
print(type(big))  # <class 'int'>

# Floats (IEEE 754 double)
pi = 3.14159
print(0.1 + 0.2)  # 0.30000000000000004

# Complex numbers
z = 3 + 4j
print(z.real, z.imag)  # 3.0 4.0
print(abs(z))          # 5.0

# Boolean is subclass of int
print(isinstance(True, int))  # True
print(True + True)            # 2

# Underscores in numbers (3.6+)
million = 1_000_000
binary = 0b_1010_1010

Constantes & Conventions de nommage

Python n'a pas de mot-clé const — les noms en MAJUSCULES sont des constantes par convention uniquement (rien n'empêche la réaffectation). PEP 8 définit le nommage : snake_case pour les variables/fonctions, PascalCase pour les classes, MAJUSCULES pour les constantes. Un tiret bas de tête signifie 'privé' par convention ; le double tiret bas déclenche la modification de nom.

python
# Python has no true constants - convention only
MAX_SIZE = 100          # ALL_CAPS for constants
PI = 3.14159

# Naming conventions (PEP 8)
variable_name = "snake_case"      # variables, functions
ClassName = "PascalCase"          # classes
CONSTANT_VALUE = 100              # constants
_private_var = "underscore prefix" # private (convention)
__name_mangled = "double underscore" # name mangling

# dunder names (reserved)
__name__, __main__, __init__
03

Chaînes

Méthodes de chaîne

Les chaînes sont immuables — les méthodes renvoient de nouvelles chaînes. find() renvoie -1 si non trouvé, tandis que index() lève une ValueError. Utilisez isalpha()/isdigit()/isalnum() pour la validation. La classe str possède plus de 40 méthodes — explorez avec dir(str).

python
s = "Hello, World"

# Case operations
print(s.upper())          # HELLO, WORLD
print(s.lower())          # hello, world
print(s.title())          # Hello, World
print(s.capitalize())     # Hello, world
print(s.swapcase())       # hELLO, wORLD

# Search & replace
print(s.find("World"))    # 7 (index, -1 if not found)
print(s.index("World"))   # 7 (raises ValueError if not found)
print(s.replace("o", "0"))  # Hell0, W0rld
print(s.count("l"))       # 3

# Validation
print("abc".isalpha())    # True
print("123".isdigit())    # True
print("  ".isspace())     # True

Formatage de chaîne

Les f-strings sont la méthode moderne, la plus rapide et la plus lisible pour formater les chaînes. Elles prennent en charge les spécifications de format après deux-points : :.2f pour 2 décimales, :>10 pour aligner à droite largeur 10, :, pour le séparateur de milliers. Évitez le formatage %- dans le nouveau code.

python
name = "Alice"
age = 30

# f-strings (Python 3.6+) - PREFERRED
print(f"Hello, {name}! You are {age}.")
print(f"{name.upper()} is {age * 365} days old")
print(f"{3.14159:.2f}")      # 3.14
print(f"{42:>10}")           # right-align
print(f"{42:<10}")           # left-align
print(f"{42:^10}")           # center
print(f"{1000000:,}")        # 1,000,000

# str.format() method
print("Hello, {}!".format(name))
print("{name} is {age}".format(name="Bob", age=25))

# Old style (avoid in new code)
print("Hello, %s!" % name)

Découpage & Indexation

La syntaxe de découpage Python [start:stop:step] est puissante — stop est exclusif. Les indices négatifs comptent depuis la fin. s[::-1] est la méthode idiomatique pour inverser une chaîne. Le découpage est sûr : les indices hors plage renvoient des chaînes vides plutôt que de lever des erreurs.

python
s = "Hello, World"

# Indexing (0-based, negative from end)
print(s[0])        # H
print(s[-1])       # d
print(s[7])        # W

# Slicing [start:stop:step]
print(s[0:5])      # Hello
print(s[7:])       # World
print(s[:5])       # Hello
print(s[::2])      # HloWrd (every 2nd char)
print(s[::-1])     # dlroW ,olleH (reverse!)

# Length
print(len(s))      # 12

# Slicing never raises IndexError
print(s[100:200])  # '' (empty string)

Division & Jointure

split() divise une chaîne en liste, join() combine une liste en chaîne. Utilisez toujours join() pour la concaténation efficace de nombreuses chaînes — l'opérateur + crée des chaînes intermédiaires. partition() divise en exactement 3 parties (avant, séparateur, après).

python
# Split
csv = "a,b,c,d"
print(csv.split(","))          # ['a', 'b', 'c', 'd']
print(csv.split(",", 2))       # ['a', 'b', 'c,d'] (max 2 splits)

# Splitlines
text = "line1\nline2\nline3"
print(text.splitlines())       # ['line1', 'line2', 'line3']

# Partition (splits on first occurrence)
print("[email protected]".partition("@"))
# ('user', '@', 'domain.com')

# Join
words = ["Hello", "World"]
print(" ".join(words))         # Hello World
print("-".join(["2024", "01", "15"]))  # 2024-01-15
print("".join(["a", "b", "c"])) # abc

# String concatenation
s = "Hello" + " " + "World"
parts = ["a"]
parts += "b"  # NOT string concat - adds chars to list!

Suppression & Remplissage

strip() supprime les espaces de tête/de fin par défaut, ou les caractères spécifiés. zfill() remplit avec des zéros en tête (utile pour les ID). rjust/ljust/center remplissent à une largeur spécifiée avec un caractère de remplissage optionnel.

python
# Strip whitespace (or specified chars)
s = "  hello  "
print(s.strip())    # "hello"
print(s.lstrip())   # "hello  "
print(s.rstrip())   # "  hello"

# Strip specific characters
print("xxxhelloxxx".strip("x"))  # hello

# Padding / centering
print("42".zfill(5))      # 00042
print("hi".rjust(10))     # "        hi"
print("hi".ljust(10, "-")) # "hi--------"
print("hi".center(10, "*")) # "****hi****"

# expandtabs
print("a\tb".expandtabs(4))  # "a   b"

Chaînes brutes & Échappements

Les chaînes brutes (r'...') traitent les barres obliques inversées littéralement — essentielles pour les motifs regex et les chemins de fichiers Windows. Les chaînes entre triples guillemets préservent les nouvelles lignes. Les chaînes prennent en charge l'opérateur * pour la répétition et + pour la concaténation.

python
# Escape sequences
print("Line1\nLine2")     # newline
print("Tab\there")        # tab
print("Quote: \"hi\"")   # escaped quotes
print("Backslash: \\")   # literal backslash

# Raw strings (ignore escapes) - great for regex
path = r"C:\Users\name\file.txt"
regex = r"\d{3}-\d{4}"
print(path)  # C:\Users\name\file.txt

# Triple-quoted strings
multi = """
Multiple
lines
"""

# String multiplication
print("ab" * 3)  # ababab
04

Nombres & Mathématiques

Opérateurs arithmétiques

Python possède 7 opérateurs arithmétiques. / renvoie toujours un flottant, // est la division entière (arrondit vers moins l'infini). ** est l'exponentiation (pas ^ — c'est XOR). Le résultat de l'opérateur % prend le signe du diviseur, contrairement à C/Java.

python
# Basic operators
print(7 + 3)   # 10  addition
print(7 - 3)   # 4   subtraction
print(7 * 3)   # 21  multiplication
print(7 / 3)   # 2.333...  true division (always float)
print(7 // 3)  # 2   floor division
print(7 % 3)   # 1   modulo (remainder)
print(7 ** 3)  # 343 exponentiation

# Floor division with negatives
print(-7 // 3)  # -3 (rounds toward negative infinity)
print(-7 % 3)   # 2 (result has same sign as divisor)

# Augmented assignment
x = 10
x += 5   # x = x + 5
x **= 2  # x = x ** 2

Module math

Le module math fournit des fonctions et constantes mathématiques. Toutes les fonctions trigonométriques utilisent des radians — convertissez avec math.radians()/degrees(). math.gcd() trouve le plus grand commun diviseur. Pour les nombres complexes, utilisez le module cmath.

python
import math

# Constants
print(math.pi)       # 3.141592653589793
print(math.e)        # 2.718281828459045
print(math.inf)      # inf
print(math.nan)      # nan

# Functions
print(math.sqrt(16))     # 4.0
print(math.pow(2, 10))   # 1024.0
print(math.log(100, 10)) # 2.0 (log base 10)
print(math.log(math.e))  # 1.0 (natural log)
print(math.factorial(5)) # 120
print(math.gcd(12, 8))   # 4

# Rounding
print(math.floor(3.7))   # 3
print(math.ceil(3.2))    # 4
print(math.trunc(-3.7))  # -3 (toward zero)

# Trigonometry (radians)
print(math.sin(math.pi / 2))  # 1.0
print(math.degrees(math.pi))  # 180.0

Module random

Le module random utilise le PRNG Mersenne Twister — PAS cryptographiquement sûr. Utilisez le module secrets pour la sécurité. random.sample() choisit des éléments uniques, random.choices() autorise les doublons. Définissez une graine pour des résultats reproductibles dans les tests.

python
import random

# Random integers
print(random.randint(1, 100))    # 1 to 100 inclusive
print(random.randrange(0, 10, 2)) # even number 0,2,4,6,8

# Random floats
print(random.random())           # 0.0 to 1.0
print(random.uniform(1.0, 10.0)) # random float in range

# Choice & sampling
colors = ["red", "green", "blue"]
print(random.choice(colors))           # one random item
print(random.sample(colors, 2))        # 2 unique items
print(random.choices(colors, k=5))     # 5 items (with replacement)

# Shuffle (in-place)
nums = [1, 2, 3, 4, 5]
random.shuffle(nums)
print(nums)

# Reproducible randomness
random.seed(42)  # same seed = same sequence

Decimal & Fractions

Utilisez Decimal pour les calculs financiers où les erreurs de précision flottante sont inacceptables (par ex. l'argent). Utilisez Fraction pour l'arithmétique rationnelle exacte. Les deux sont plus lents que float mais évitent les erreurs d'arrondi. Construisez toujours Decimal à partir de chaînes, pas de flottants.

python
from decimal import Decimal, getcontext
from fractions import Fraction

# Float precision issues
print(0.1 + 0.2)  # 0.30000000000000004

# Decimal for exact decimal arithmetic
a = Decimal("0.1")
b = Decimal("0.2")
print(a + b)  # 0.3 (exact!)

# Set precision
getcontext().prec = 6
print(Decimal(1) / Decimal(7))  # 0.142857

# Fractions for exact rational arithmetic
f1 = Fraction(1, 3)
f2 = Fraction(1, 6)
print(f1 + f2)  # 1/2
print(float(f1))  # 0.3333...

# Fraction from string
print(Fraction("3/4"))  # 3/4

Opérateurs bit à bit

Les opérateurs bit à bit manipulent les bits individuels des entiers. Les entiers Python ont une précision arbitraire, donc les décalages fonctionnent différemment des langages à largeur fixe. Utilisations courantes : indicateurs, masques, analyse de protocoles de bas niveau. x & (x-1) == 0 vérifie si x est une puissance de 2.

python
# Bitwise operators work on integers
a = 0b1010  # 10
b = 0b1100  # 12

print(a & b)   # 8  (0b1000) AND
print(a | b)   # 14 (0b1110) OR
print(a ^ b)   # 6  (0b0110) XOR
print(~a)      # -11 (NOT, two's complement)
print(a << 2)  # 40 (left shift, multiply by 4)
print(a >> 1)  # 5  (right shift, divide by 2)

# Binary representation
print(bin(10))   # 0b1010
print(hex(255))  # 0xff
print(oct(8))    # 0o10
print(int("1010", 2))  # 10 (parse binary)

# Common tricks
print(5 & 1)     # 1 (check odd: nonzero = odd)
print(8 & (8-1)) # 0 (check power of 2)
05

Structures de données

Listes

Les listes sont la structure de données la plus polyvalente de Python — ordonnées, mutables et hétérogènes. append() est O(1), insert(0, x) est O(n). Utilisez collections.deque pour des opérations rapides aux deux extrémités. sort() est sur place, sorted() renvoie une nouvelle liste.

python
# Lists are ordered, mutable sequences
nums = [1, 2, 3, 4, 5]
mixed = [1, "hello", True, 3.14]

# Adding elements
nums.append(6)           # [1,2,3,4,5,6]
nums.insert(0, 0)        # [0,1,2,3,4,5,6]
nums.extend([7, 8])      # extend with another list

# Removing elements
nums.remove(0)           # remove by value
popped = nums.pop()      # remove & return last
popped = nums.pop(0)     # remove & return by index
del nums[0]              # delete by index
nums.clear()             # remove all

# Slicing (same as strings)
nums = [1, 2, 3, 4, 5]
print(nums[1:3])    # [2, 3]
print(nums[::-1])   # [5, 4, 3, 2, 1] reverse

# Sorting
nums.sort()              # in-place sort
nums.sort(reverse=True)  # descending
sorted_nums = sorted(nums)  # returns new list

Tuples

Les tuples sont immuables et plus rapides que les listes. Utilisez-les pour les collections fixes, les valeurs de retour multiples et les clés de dictionnaire (les listes ne peuvent pas être des clés). Les tuples nommés fournissent des noms de champs pour la lisibilité. Les tuples à élément unique nécessitent une virgule finale.

python
# Tuples are ordered, IMMUTABLE sequences
point = (3, 4)
single = (42,)     # note the comma for single-element tuple
empty = ()

# Packing & unpacking
coordinates = 10, 20, 30  # packing
x, y, z = coordinates     # unpacking
x, y = y, x               # swap values!

# Multiple return values
def min_max(nums):
    return min(nums), max(nums)

lo, hi = min_max([3, 1, 4, 1, 5])

# Named tuples (readable)
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p.x, p.y)       # 3 4
print(p[0], p[1])     # 3 4

# Tuples are immutable but can contain mutable objects
t = (1, [2, 3])
t[1].append(4)  # OK: (1, [2, 3, 4])

Dictionnaires

Les dictionnaires sont des tables de hachage — O(1) en moyenne pour recherche/insertion/suppression. Les clés doivent être hachables (immuables). Depuis Python 3.7, les dicts maintiennent l'ordre d'insertion. Utilisez get() pour éviter KeyError. La compréhension de dict crée des dicts élégamment.

python
# Dicts are key-value mappings (insertion-ordered since 3.7)
user = {"name": "Alice", "age": 30}

# Access
print(user["name"])          # Alice
print(user.get("email"))     # None (no KeyError)
print(user.get("email", "N/A"))  # N/A (default)

# Add/update
user["email"] = "[email protected]"    # add
user["age"] = 31             # update
user.setdefault("role", "user")  # set if missing

# Delete
del user["email"]
val = user.pop("age")        # remove & return
# user.clear()               # remove all

# Iteration
for key in user:             # keys
    print(key)
for k, v in user.items():    # key-value pairs
    print(k, v)
for v in user.values():      # values
    print(v)

# Dict comprehension
squares = {x: x**2 for x in range(5)}

# Merge dicts (3.9+)
merged = {"a": 1} | {"b": 2}

Ensembles

Les ensembles sont des collections non ordonnées d'éléments uniques et hachables. Ils excellent dans les tests d'appartenance (O(1) contre O(n) pour les listes) et l'algèbre d'ensembles (union, intersection, différence). frozenset est immuable et hachable. L'ordre n'est pas garanti.

python
# Sets are unordered collections of unique elements
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

# Set operations
print(a | b)    # union: {1, 2, 3, 4, 5, 6}
print(a & b)    # intersection: {3, 4}
print(a - b)    # difference: {1, 2}
print(a ^ b)    # symmetric difference: {1, 2, 5, 6}

# Methods
a.add(5)        # add element
a.discard(10)   # remove if present (no error)
a.remove(1)     # remove (KeyError if missing)
a.update([6, 7]) # add multiple

# Membership test (O(1) - faster than list)
print(3 in a)   # True

# Frozen set (immutable)
fs = frozenset([1, 2, 3])

# Common use: deduplicate
unique = list(set([1, 1, 2, 2, 3]))  # [1, 2, 3]

Compréhensions

Les compréhensions sont une manière pythonique de créer des collections de façon concise. Les expressions génératrices (parenthèses au lieu de crochets) sont paresseuses — elles produisent des valeurs à la demande, économisant de la mémoire. Préférez les compréhensions à map()/filter() pour la lisibilité.

python
# List comprehension
squares = [x**2 for x in range(10)]
evens = [x for x in range(20) if x % 2 == 0]
pairs = [(x, y) for x in range(3) for y in range(3)]

# Dict comprehension
square_map = {x: x**2 for x in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# Set comprehension
unique_lens = {len(w) for w in ["a", "ab", "abc", "ab"]}

# Generator expression (lazy, memory-efficient)
gen = (x**2 for x in range(1000000))
print(next(gen))  # 0
print(next(gen))  # 1
total = sum(x**2 for x in range(100))  # no extra list

# Nested comprehension (matrix)
matrix = [[i * 3 + j for j in range(3)] for i in range(3)]
# [[0,1,2], [3,4,5], [6,7,8]]

Module collections

Le module collections fournit des conteneurs spécialisés. Counter compte les éléments hachables. defaultdict crée automatiquement les clés manquantes. deque offre append/pop en O(1) aux deux extrémités (contre O(n) pour les listes). Ils sont essentiels pour un code propre et efficace.

python
from collections import Counter, defaultdict, deque, OrderedDict

# Counter - counting
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
cnt = Counter(words)
print(cnt)  # Counter({'apple': 3, 'banana': 2, 'cherry': 1})
print(cnt.most_common(2))  # [('apple', 3), ('banana', 2)]

# defaultdict - no KeyError
dd = defaultdict(list)
dd["fruits"].append("apple")
dd["fruits"].append("banana")
# dd["vegs"] automatically creates empty list

# deque - fast double-ended queue
dq = deque([1, 2, 3])
dq.appendleft(0)   # [0, 1, 2, 3]
dq.append(4)       # [0, 1, 2, 3, 4]
dq.popleft()       # 0, deque is now [1, 2, 3, 4]
dq.rotate(1)       # rotate right

# OrderedDict (less needed since 3.7, dicts are ordered)
od = OrderedDict([("a", 1), ("b", 2)])
06

Flux de contrôle

If / Elif / Else

Python utilise if/elif/else — notez 'elif' et non 'elseif'. L'indentation définit les blocs. Le ternaire 'x if cond else y' est une expression. Python traite les collections vides, 0, None et False comme faux — utile pour des conditions concises.

python
score = 85

if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 70:
    grade = "C"
else:
    grade = "F"

print(f"Grade: {grade}")  # Grade: B

# Conditional expression (ternary)
status = "pass" if score >= 60 else "fail"

# Truthy/falsy values
# Falsy: False, 0, 0.0, "", [], {}, (), None
# Everything else is truthy
if []:    # False
    print("never")
if [0]:   # True (non-empty list)
    print("always")

Boucles for & Itération

La boucle for de Python itère sur tout itérable. range() génère des nombres (stop exclusif). enumerate() associe les éléments à des indices. zip() itère plusieurs séquences en parallèle. Utilisez .items() pour itérer les paires clé-valeur d'un dict.

python
# range(start, stop, step)
for i in range(5):        # 0, 1, 2, 3, 4
    print(i)
for i in range(2, 10, 2): # 2, 4, 6, 8
    print(i)
for i in range(10, 0, -1): # countdown
    print(i)

# Iterate over collections
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    print(fruit)

# enumerate for index + value
for idx, fruit in enumerate(fruits):
    print(f"{idx}: {fruit}")

# zip to iterate multiple sequences
names = ["Alice", "Bob"]
ages = [30, 25]
for name, age in zip(names, ages):
    print(f"{name}: {age}")

# Iterate dict
user = {"name": "Alice", "age": 30}
for key, value in user.items():
    print(f"{key} = {value}")

Boucles while & Break/Continue

Les boucles while répètent tant qu'une condition est vraie. break sort de la boucle immédiatement, continue passe à l'itération suivante. La construction for/else exécute le bloc else uniquement si la boucle n'a pas été interrompue. pass est un placeholder no-op pour les blocs vides.

python
# Basic while
count = 0
while count < 5:
    print(count)
    count += 1

# break - exit loop
while True:
    cmd = input("> ")
    if cmd == "quit":
        break
    print(f"You said: {cmd}")

# continue - skip to next iteration
for i in range(10):
    if i % 2 == 0:
        continue  # skip even numbers
    print(i)  # prints 1, 3, 5, 7, 9

# else clause (runs if no break)
for i in range(5):
    if i == 10:
        break
else:
    print("Loop completed without break")

# pass - do nothing (placeholder)
for i in range(5):
    pass  # TODO: implement

Instruction match (Python 3.10+)

L'instruction match (Python 3.10+) est un filtrage par motif structurel puissant, bien au-delà du switch de C. Elle peut filtrer des séquences, des mappings, des instances de classe et lier des variables. Le motif _ est un joker (par défaut). Les gardes avec 'if' ajoutent des conditions.

python
# Structural pattern matching (like switch)
def handle_command(cmd):
    match cmd.split():
        case ["quit"]:
            return "Goodbye"
        case ["hello", name]:
            return f"Hello, {name}!"
        case ["move", direction] if direction in "NSEW":
            return f"Moving {direction}"
        case ["add", x, y]:
            return int(x) + int(y)
        case _:
            return "Unknown command"

print(handle_command("hello Alice"))  # Hello, Alice!

# Matching data structures
match point:
    case (0, 0):
        print("origin")
    case (0, y):
        print(f"on y-axis at {y}")
    case (x, 0):
        print(f"on x-axis at {x}")
    case (x, y):
        print(f"at ({x}, {y})")

Itérateurs & Générateurs

Les générateurs produisent des valeurs paresseusement avec yield — ils ne calculent pas toutes les valeurs à l'avance, économisant de la mémoire. Ils implémentent le protocole itérateur (iter() et next()). Une fois épuisés, ils sont terminés. Utilisez les générateurs pour les séquences grandes/infinies, les pipelines et les flux de données.

python
# Iterator protocol
nums = [1, 2, 3]
it = iter(nums)
print(next(it))  # 1
print(next(it))  # 2
print(next(it))  # 3
# next(it)  # StopIteration

# Generator function (uses yield)
def count_up_to(n):
    count = 1
    while count <= n:
        yield count
        count += 1

for num in count_up_to(5):
    print(num)  # 1, 2, 3, 4, 5

# Infinite generator
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
print(next(fib))  # 0
print(next(fib))  # 1
print(next(fib))  # 1
print(next(fib))  # 2

# Generator expression
squares = (x**2 for x in range(10))
07

Fonctions

Définir & Appeler des fonctions

Les fonctions sont définies avec def. Les arguments par défaut utilisent =. Python prend en charge les arguments nommés pour la clarté. Les fonctions peuvent renvoyer plusieurs valeurs (sous forme de tuple). Les docstrings (triples guillemets) documentent les fonctions et sont accessibles via help().

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

print(greet("Alice"))  # Hello, Alice!

# Default arguments
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

print(greet("Bob"))              # Hello, Bob!
print(greet("Bob", "Hi"))        # Hi, Bob!

# Keyword arguments
print(greet(name="Carol", greeting="Hey"))

# Return multiple values (tuple)
def stats(nums):
    return min(nums), max(nums), sum(nums) / len(nums)

lo, hi, avg = stats([1, 2, 3, 4, 5])

# Docstrings
def add(a, b):
    """Add two numbers and return the result.
    
    Args:
        a: First number
        b: Second number
    Returns:
        Sum of a and b
    """
    return a + b

Arguments : *args & **kwargs

*args collecte les arguments positionnels supplémentaires dans un tuple, **kwargs collecte les arguments nommés supplémentaires dans un dict. Les noms args/kwargs sont conventionnels. Vous pouvez déballer des séquences avec * et des dicts avec ** lors de l'appel de fonctions. Ordre : positionnels, *args, nommés, **kwargs.

python
# *args - variable positional arguments (tuple)
def sum_all(*args):
    return sum(args)

print(sum_all(1, 2, 3))       # 6
print(sum_all(1, 2, 3, 4, 5)) # 15

# **kwargs - variable keyword arguments (dict)
def print_info(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_info(name="Alice", age=30, role="admin")

# Combining all
def func(a, b, *args, **kwargs):
    print(f"a={a}, b={b}")
    print(f"args={args}")
    print(f"kwargs={kwargs}")

func(1, 2, 3, 4, x=5, y=6)
# a=1, b=2, args=(3, 4), kwargs={'x': 5, 'y': 6}

# Unpacking arguments
nums = [1, 2, 3]
print(sum_all(*nums))  # unpack list as args

opts = {"name": "Alice", "age": 30}
print_info(**opts)  # unpack dict as kwargs

Lambda & Fonctions d'ordre supérieur

Les lambdas sont limités à une seule expression — utilisez def pour une logique complexe. Ils brillent comme arguments de fonctions d'ordre supérieur comme sorted(), map(), filter(). Cependant, les compréhensions de liste sont souvent plus lisibles que map/filter. reduce() se trouve dans functools.

python
# Lambda - anonymous function (single expression)
square = lambda x: x ** 2
print(square(5))  # 25

# Common with sorted, map, filter, reduce
students = [("Alice", 85), ("Bob", 92), ("Carol", 78)]

# Sort by score (key function)
sorted_by_score = sorted(students, key=lambda s: s[1])
# [('Carol', 78), ('Alice', 85), ('Bob', 92)]

# map - apply function to each item
nums = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, nums))
# [2, 4, 6, 8, 10]

# filter - keep items where function returns True
evens = list(filter(lambda x: x % 2 == 0, nums))
# [2, 4]

# reduce - accumulate to single value
from functools import reduce
product = reduce(lambda a, b: a * b, nums)
# 120 (1*2*3*4*5)

# Prefer comprehensions over map/filter
doubled = [x * 2 for x in nums]  # more Pythonic
evens = [x for x in nums if x % 2 == 0]

Décorateurs

Les décorateurs enveloppent des fonctions pour ajouter du comportement sans modifier le code original. La syntaxe @ est du sucre syntaxique. Les décorateurs avec arguments nécessitent un niveau d'imbrication supplémentaire. Utilisez toujours functools.wraps pour préserver les métadonnées de la fonction originale (nom, docstring).

python
# A decorator modifies a function's behavior
def uppercase_result(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

@uppercase_result
def greet(name):
    return f"hello, {name}"

print(greet("alice"))  # HELLO, ALICE

# Decorator with arguments
def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def say_hi():
    print("Hi!")

say_hi()  # prints "Hi!" three times

# Practical: timing decorator
import time
def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.time() - start:.4f}s")
        return result
    return wrapper

# Use functools.wraps to preserve metadata
from functools import wraps
def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

Portée & Fermetures

Python résout les noms en utilisant l'ordre de portée LEGB : Local, Englobante, Globale, Intégrée. Utilisez 'global' pour rebinder une variable globale dans une fonction. Utilisez 'nonlocal' (Python 3) pour modifier une variable dans une portée englobante. Les fermetures se souviennent de leur portée englobante.

python
# LEGB rule: Local, Enclosing, Global, Built-in
x = "global"

def outer():
    x = "enclosing"
    
    def inner():
        x = "local"
        print(x)  # local
    
    inner()
    print(x)  # enclosing

outer()
print(x)  # global

# global keyword - modify global variable
count = 0
def increment():
    global count
    count += 1

# nonlocal keyword - modify enclosing variable
def make_counter():
    count = 0
    def counter():
        nonlocal count
        count += 1
        return count
    return counter

c = make_counter()
print(c())  # 1
print(c())  # 2
print(c())  # 3
08

POO & Classes

Classes & Objets

Les classes regroupent des données (attributs) et du comportement (méthodes). __init__ est le constructeur. self fait référence à l'instance (comme 'this' dans d'autres langages). Les variables de classe sont partagées ; les variables d'instance sont par objet. __str__ est pour les utilisateurs, __repr__ est pour les développeurs.

python
class Dog:
    # Class variable (shared by all instances)
    species = "Canis familiaris"
    
    # Constructor
    def __init__(self, name, age):
        # Instance variables
        self.name = name
        self.age = age
    
    # Instance method
    def bark(self):
        return f"{self.name} says Woof!"
    
    # String representation
    def __str__(self):
        return f"Dog({self.name}, {self.age})"
    
    # Official representation (for debugging)
    def __repr__(self):
        return f"Dog(name='{self.name}', age={self.age})"

# Create instances
buddy = Dog("Buddy", 3)
lucy = Dog("Lucy", 5)

print(buddy.bark())       # Buddy says Woof!
print(buddy.name)         # Buddy
print(buddy.species)      # Canis familiaris
print(str(buddy))         # Dog(Buddy, 3)

Héritage & Polymorphisme

L'héritage permet aux classes de réutiliser et d'étendre le comportement. Python prend en charge l'héritage multiple avec MRO (Method Resolution Order) pour résoudre les conflits. Le polymorphisme permet de traiter différents types uniformément. Utilisez isinstance() pour la vérification de type, pas type().

python
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        raise NotImplementedError("Subclass must implement")

class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

# Polymorphism - same interface, different behavior
def animal_sound(animal):
    print(animal.speak())

animals = [Dog("Buddy"), Cat("Whiskers")]
for a in animals:
    animal_sound(a)
# Buddy says Woof!
# Whiskers says Meow!

# Multiple inheritance
class Swimmer:
    def swim(self):
        return "swimming"

class Flyer:
    def fly(self):
        return "flying"

class Duck(Animal, Swimmer, Flyer):
    pass

duck = Duck("Donald")
print(duck.swim())  # swimming
print(duck.fly())   # flying

# Check inheritance
print(isinstance(duck, Animal))  # True
print(issubclass(Dog, Animal))   # True

Propriétés & Encapsulation

Python n'a pas de véritable privé/protégé — il utilise des conventions. Un seul tiret bas _ signifie 'interne'. Le double tiret bas __ déclenche la modification de nom (pas une véritable confidentialité). @property transforme les méthodes en attributs avec getters/setters, permettant la validation et les propriétés calculées.

python
class Temperature:
    def __init__(self, celsius=0):
        self.celsius = celsius  # uses setter below
    
    # Getter
    @property
    def celsius(self):
        return self._celsius
    
    # Setter
    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Below absolute zero!")
        self._celsius = value
    
    # Computed property
    @property
    def fahrenheit(self):
        return self._celsius * 9/5 + 32
    
    @fahrenheit.setter
    def fahrenheit(self, value):
        self.celsius = (value - 32) * 5/9

temp = Temperature(25)
print(temp.fahrenheit)  # 77.0
temp.fahrenheit = 100
print(temp.celsius)     # 37.78...

# Name conventions:
# _name   - protected (convention, not enforced)
# __name  - private (name mangling: _ClassName__name)
# __name__ - dunder (reserved by Python)

Méthodes de classe & statiques

@staticmethod est juste une fonction dans l'espace de noms d'une classe — aucun premier argument implicite. @classmethod reçoit la classe (cls) comme premier argument, utile pour les constructeurs alternatifs (méthodes de fabrique) et le comportement sensible à l'héritage. Utilisez classmethod pour les constructeurs, staticmethod pour les fonctions utilitaires.

python
class MathUtils:
    pi = 3.14159
    
    # Static method - no self/cls, lives in class namespace
    @staticmethod
    def add(a, b):
        return a + b
    
    # Class method - receives the class as first argument
    @classmethod
    def circle_area(cls, radius):
        return cls.pi * radius ** 2
    
    # Alternative constructor (common classmethod use)
    @classmethod
    def from_diameter(cls, diameter):
        return cls()  # would configure instance

# Static: called on class or instance, no special first arg
print(MathUtils.add(2, 3))  # 5

# Class: often used for alternative constructors
print(MathUtils.circle_area(5))  # 78.54...

# Factory pattern with classmethod
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    @classmethod
    def origin(cls):
        return cls(0, 0)
    
    @classmethod
    def from_tuple(cls, coords):
        return cls(*coords)

p1 = Point.origin()
p2 = Point.from_tuple((3, 4))

Méthodes magiques (Dunder)

Les méthodes magiques (méthodes dunder) implémentent la surcharge d'opérateurs et le comportement de protocole. __add__ pour +, __eq__ pour ==, __len__ pour len(), __iter__ pour l'itération/le déballage. Elles permettent à vos objets de fonctionner naturellement avec la syntaxe et les fonctions intégrées de Python.

python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    # String representation
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
    
    # Operator overloading
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)
    
    # Equality
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
    
    # Length
    def __len__(self):
        return int((self.x**2 + self.y**2) ** 0.5)
    
    # Make it iterable
    def __iter__(self):
        yield self.x
        yield self.y
    
    # Index access
    def __getitem__(self, index):
        return (self.x, self.y)[index]

v1 = Vector(2, 3)
v2 = Vector(4, 5)
print(v1 + v2)        # Vector(6, 8)
print(v1 * 3)         # Vector(6, 9)
print(v1 == Vector(2, 3))  # True
print(len(v1))        # 3
x, y = v1             # unpacking via __iter__
09

Gestion des erreurs

Try / Except / Finally

try/except/else/finally : try exécute le code risqué, except capture les erreurs, else s'exécute si aucune exception, finally s'exécute toujours (nettoyage). Capturez des exceptions spécifiques, pas un 'except:' nu. Le bloc else est utile lorsque le nettoyage ne doit se produire qu'en cas de succès. Exception est la base de la plupart des erreurs capturables.

python
# Basic exception handling
try:
    result = 10 / 0
except ZeroDivisionError as e:
    print(f"Error: {e}")  # division by zero
finally:
    print("This always runs")

# Multiple exception types
try:
    value = int("abc")
except (ValueError, TypeError) as e:
    print(f"Conversion error: {e}")

# Different handlers for different exceptions
try:
    f = open("nonexistent.txt")
    data = f.read()
except FileNotFoundError:
    print("File not found")
except PermissionError:
    print("No permission")
except Exception as e:
    print(f"Unexpected: {e}")
else:
    print("No exception occurred")
    f.close()
finally:
    print("Cleanup (always runs)")

# Exception hierarchy
# BaseException
#  ├── SystemExit
#  ├── KeyboardInterrupt
#  └── Exception
#       ├── ValueError
#       ├── TypeError
#       ├── KeyError
#       └── ...

Lever des exceptions

Utilisez raise pour lancer des exceptions. 'raise' seul relève l'exception courante (dans un bloc except). 'raise X from Y' chaîne les exceptions, préservant la cause originale. Levez toujours des types d'exception spécifiques. Évitez d'utiliser des exceptions pour le flux de contrôle normal.

python
# Raise an exception
def divide(a, b):
    if b == 0:
        raise ZeroDivisionError("Cannot divide by zero!")
    return a / b

# Re-raise the current exception
def process(data):
    try:
        return parse(data)
    except ValueError:
        print("Logging parse error...")
        raise  # re-raises the same exception

# Raise with context (from)
try:
    int("abc")
except ValueError as e:
    raise RuntimeError("Failed to process input") from e

# Common built-in exceptions
raise ValueError("invalid value")
raise TypeError("wrong type")
raise KeyError("missing key")
raise IndexError("out of range")
raise RuntimeError("something went wrong")
raise NotImplementedError("override this")
raise FileNotFoundError("no such file")

# Exception with custom args
class ValidationError(Exception):
    pass

raise ValidationError("field is required", "email")

Exceptions personnalisées

Créez des exceptions personnalisées en sous-classant Exception (ou un intégré plus spécifique). Concevez une hiérarchie pour que les appelants puissent capturer au bon niveau. Ajoutez des attributs personnalisés pour transporter du contexte. Héritez d'Exception, pas de BaseException (qui inclut SystemExit/KeyboardInterrupt).

python
# Custom exception hierarchy
class AppError(Exception):
    """Base exception for the application."""
    pass

class DatabaseError(AppError):
    def __init__(self, message, query=None):
        super().__init__(message)
        self.query = query

class ValidationError(AppError):
    def __init__(self, field, message):
        super().__init__(f"{field}: {message}")
        self.field = field
        self.message = message

class AuthenticationError(AppError):
    pass

# Usage
def login(username, password):
    if not username:
        raise ValidationError("username", "is required")
    if password != "secret":
        raise AuthenticationError("Invalid credentials")

# Catching by hierarchy
try:
    login("", "x")
except ValidationError as e:
    print(f"Validation failed: {e.field}")
except AppError as e:
    print(f"App error: {e}")

# Access exception info
import traceback
try:
    1 / 0
except:
    traceback.print_exc()
    print(repr(sys.exc_info()[1]))

Gestionnaires de contexte (instruction with)

Les gestionnaires de contexte (instruction 'with') garantissent le nettoyage via __enter__ et __exit__. Ils sont essentiels pour les ressources comme les fichiers, les verrous et les connexions de base de données. contextlib.contextmanager simplifie la création en utilisant un générateur. __exit__ peut supprimer les exceptions en renvoyant True.

python
# Context managers handle setup and cleanup
with open("file.txt") as f:
    content = f.read()
# file is automatically closed, even if an error occurs

# Multiple context managers
with open("input.txt") as fin, open("output.txt", "w") as fout:
    fout.write(fin.read())

# Creating a context manager (class-based)
class Timer:
    def __enter__(self):
        import time
        self.start = time.time()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        self.elapsed = time.time() - self.start
        print(f"Elapsed: {self.elapsed:.4f}s")
        return False  # don't suppress exceptions

with Timer() as t:
    # code to time
    sum(range(1000000))

# contextlib for simpler context managers
from contextlib import contextmanager

@contextmanager
def open_db(url):
    db = connect(url)
    try:
        yield db
    finally:
        db.close()

with open_db("localhost") as db:
    db.query("SELECT 1")

Assertions & Journalisation

Les instructions assert servent à déboguer les invariants — elles sont supprimées lorsque Python s'exécute avec -O (optimisation). N'utilisez jamais d'assertions pour la validation d'entrée. Utilisez le module logging au lieu de print() pour le code de production — il prend en charge les niveaux, le formatage et les destinations de sortie.

python
# Assertions - for debugging (removed with -O flag)
def divide(a, b):
    assert b != 0, "Divisor cannot be zero"
    return a / b

# Never use assertions for data validation (they can be disabled)
# Use them for invariant checks during development

# Logging (better than print for production)
import logging

# Configure logging
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [%(levelname)s] %(message)s"
)

logger = logging.getLogger(__name__)

logger.debug("Detailed info for debugging")
logger.info("Confirmation things are working")
logger.warning("Something unexpected happened")
logger.error("A serious problem")
logger.critical("A fatal error")

# Log exceptions with traceback
try:
    1 / 0
except:
    logger.exception("Failed to divide")  # includes traceback
10

E/S de fichiers

Lire des fichiers

Utilisez toujours 'with' pour ouvrir des fichiers — il les ferme automatiquement même si une erreur se produit. Spécifiez encoding='utf-8' pour éviter les problèmes d'encodage dépendants de la plateforme. Pour les gros fichiers, itérez ligne par ligne au lieu de read() pour économiser de la mémoire. readlines() charge tout le fichier en mémoire.

python
# Read entire file
with open("file.txt", "r", encoding="utf-8") as f:
    content = f.read()
    print(content)

# Read line by line (memory-efficient for large files)
with open("file.txt", "r") as f:
    for line in f:
        print(line.strip())  # strip removes trailing newline

# Read all lines into a list
with open("file.txt") as f:
    lines = f.readlines()  # ['line1\n', 'line2\n', ...]

# Read specific number of characters
with open("file.txt") as f:
    chunk = f.read(100)  # first 100 chars

# File modes:
# "r"  read (default)
# "w"  write (truncate)
# "a"  append
# "x"  exclusive create (fails if exists)
# "b"  binary mode (e.g., "rb", "wb")
# "+"  read and write (e.g., "r+")

Écrire des fichiers

Le mode 'w' tronque le fichier (supprime le contenu) ; utilisez 'a' pour ajouter. writelines() n'ajoute pas de nouvelles lignes — ajoutez-les manuellement. Utilisez 'rb'/'wb' pour les fichiers binaires (images, etc.). seek() déplace le curseur ; tell() renvoie sa position. Spécifiez toujours l'encodage pour les fichiers texte.

python
# Write text (overwrites existing)
with open("output.txt", "w") as f:
    f.write("First line\n")
    f.write("Second line\n")
    
    # writelines doesn't add newlines
    f.writelines(["line3\n", "line4\n"])

# Append to a file
with open("log.txt", "a") as f:
    f.write("New log entry\n")

# Write binary data
with open("data.bin", "wb") as f:
    f.write(b"\x00\x01\x02\x03")

# Read and write simultaneously
with open("file.txt", "r+") as f:
    content = f.read()
    f.seek(0)           # move to beginning
    f.write("Updated")  # overwrite
    f.truncate()        # cut off remaining

# Check if file exists
import os
if os.path.exists("file.txt"):
    print("File exists")

Gestion des chemins (pathlib)

pathlib (Python 3.4+) est la méthode moderne et orientée objet pour gérer les chemins — préférez-le à os.path. L'opérateur / joint les chemins de manière indépendante de la plateforme. Les objets Path ont des méthodes read_text()/write_text() qui gèrent l'ouverture/fermeture pour vous. rglob() recherche récursivement.

python
from pathlib import Path

# Create Path objects (preferred over os.path)
p = Path("src/main.py")
home = Path.home()           # /home/user or C:\Users\user
cwd = Path.cwd()             # current working directory

# Path components
print(p.name)        # main.py
print(p.stem)        # main
print(p.suffix)      # .py
print(p.parent)      # src
print(p.parts)       # ('src', 'main.py')

# Joining paths (use / operator)
config = home / ".config" / "app" / "config.json"

# Existence and type
print(p.exists())    # True/False
print(p.is_file())
print(p.is_dir())

# Listing directories
for f in Path(".").iterdir():
    print(f)

# Glob patterns
for py_file in Path(".").rglob("*.py"):
    print(py_file)

# Create directories
Path("new/dir").mkdir(parents=True, exist_ok=True)

# Read/write (Path methods)
content = Path("file.txt").read_text()
Path("output.txt").write_text("Hello!")

JSON

json.dumps() sérialise en chaîne, json.loads() désérialise. dump()/load() travaillent avec des fichiers. Utilisez indent pour un affichage joli. Les objets personnalisés nécessitent un sérialiseur par défaut. JSON ne prend en charge que les types de base — utilisez default= pour datetime et autres objets complexes.

python
import json

# Python dict to JSON string
data = {"name": "Alice", "age": 30, "skills": ["Python", "SQL"]}
json_str = json.dumps(data, indent=2)
print(json_str)

# JSON string to Python dict
parsed = json.loads('{"name": "Bob", "active": true}')
print(parsed["name"])  # Bob
print(parsed["active"])  # True (Python bool)

# Write JSON to file
with open("data.json", "w") as f:
    json.dump(data, f, indent=2, ensure_ascii=False)

# Read JSON from file
with open("data.json") as f:
    loaded = json.load(f)

# Custom serialization (e.g., datetime)
from datetime import datetime
def json_default(obj):
    if isinstance(obj, datetime):
        return obj.isoformat()
    raise TypeError

json.dumps({"time": datetime.now()}, default=json_default)

# Type mapping:
# JSON object  <-> Python dict
# JSON array   <-> Python list
# JSON string  <-> Python str
# JSON number  <-> Python int/float
# JSON boolean <-> Python bool
# JSON null    <-> Python None

CSV & Autres formats

Le module csv gère CSV avec des guillemets corrects. Utilisez newline='' lors de l'ouverture de fichiers CSV sur Windows. DictReader/DictWriter travaillent avec des noms de colonnes. pickle peut sérialiser tout objet Python mais est spécifique à Python et non sécurisé — ne dépicklez jamais de données provenant de sources non fiables.

python
import csv

# Write CSV
with open("data.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["name", "age", "city"])
    writer.writerows([
        ["Alice", 30, "NYC"],
        ["Bob", 25, "LA"],
    ])

# Read CSV
with open("data.csv") as f:
    reader = csv.reader(f)
    header = next(reader)  # first row
    for row in reader:
        print(row)  # ['Alice', '30', 'NYC']

# DictReader/DictWriter (column access by name)
with open("data.csv") as f:
    reader = csv.DictReader(f)
    for row in reader:
        print(row["name"], row["age"])

# Pickle (Python-specific, can store any object)
import pickle
with open("data.pkl", "wb") as f:
    pickle.dump({"complex": [1, 2, {"a": 3}]}, f)

with open("data.pkl", "rb") as f:
    obj = pickle.load(f)

# WARNING: pickle is insecure - never unpickle untrusted data!
11

Modules & Packages

Importer des modules

Les imports apportent des modules. 'import X' garde l'espace de noms propre. 'from X import Y' est pratique mais peut causer des collisions de noms. Les alias (import X as Y) sont courants pour les bibliothèques avec des conventions (np, pd). Évitez 'from X import *' — cela pollue l'espace de noms.

python
# Import entire module
import math
print(math.sqrt(16))

# Import specific names
from datetime import datetime, timedelta
now = datetime.now()

# Import with alias
import numpy as np
import pandas as pd

# Import all names (discouraged - pollutes namespace)
# from os import *

# Conditional import (try/except)
try:
    import cjson as json
except ImportError:
    import json

# Check what's in a module
import os
print(dir(os))         # list all attributes
print(os.__file__)     # module location
print(os.__name__)     # module name

# Reload a module (during development)
import importlib
importlib.reload(my_module)

Créer des modules & packages

Un module est un fichier .py ; un package est un répertoire avec __init__.py. Le fichier __init__.py peut être vide ou configurer le package. __all__ dans __init__.py contrôle ce que 'from package import *' exporte. Python moderne (3.3+) prend en charge les packages d'espace de noms sans __init__.py.

python
# A module is just a .py file
# mymath.py
def add(a, b):
    return a + b

PI = 3.14159

# A package is a directory with __init__.py
# mypackage/
#   __init__.py
#   module1.py
#   module2.py
#   subpackage/
#     __init__.py
#     module3.py

# __init__.py can be empty or contain package initialization
# mypackage/__init__.py
from .module1 import ClassA
from .module2 import func_b
__version__ = "1.0.0"
__all__ = ["ClassA", "func_b"]

# Using the package
from mypackage import ClassA
from mypackage.subpackage import module3

# __all__ controls 'from package import *'
# Without __all__, * imports only what's in __init__.py

__name__ == '__main__'

L'idiome if __name__ == '__main__' permet à un fichier de servir à la fois de script et de module. Lorsqu'il est exécuté directement, __name__ est '__main__' ; lorsqu'il est importé, c'est le nom du module. Ce motif est essentiel pour créer des modules réutilisables qui peuvent aussi être exécutés seuls.

python
# script.py
def main():
    print("Running main")

def helper():
    print("Helper function")

if __name__ == "__main__":
    # This code only runs when the file is executed directly
    # NOT when imported as a module
    main()

# When you run: python script.py
# __name__ is "__main__" -> main() runs

# When you: import script
# __name__ is "script" -> main() does NOT run
# This lets the module be both a script and an importable library

# Common pattern for CLI tools
def main():
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("--name", required=True)
    args = parser.parse_args()
    print(f"Hello, {args.name}!")

if __name__ == "__main__":
    main()

Points forts de la bibliothèque standard

La bibliothèque standard de Python est énorme et complète. os/sys pour l'interaction système, datetime pour les dates, collections pour les conteneurs spécialisés, itertools pour les outils d'itérateur, functools pour la programmation fonctionnelle. Explorez la documentation sur docs.python.org/3/library/.

python
# os - operating system interface
import os
os.getcwd()           # current directory
os.listdir(".")       # list files
os.environ.get("HOME")  # environment variables

# sys - system-specific
import sys
sys.argv              # command-line arguments
sys.exit(0)           # exit with status code
sys.path              # module search path

# datetime - date and time
from datetime import datetime, timedelta
now = datetime.now()

# collections - specialized containers
from collections import Counter, defaultdict, deque

# itertools - iterator tools
from itertools import chain, cycle, repeat, product

# functools - higher-order functions
from functools import lru_cache, reduce, partial

# typing - type hints
from typing import List, Dict, Optional, Union, Any

# pathlib - path handling
from pathlib import Path

# subprocess - run external commands
import subprocess
result = subprocess.run(["ls", "-l"], capture_output=True, text=True)

Pip & Environnements virtuels

Utilisez toujours des environnements virtuels pour isoler les dépendances du projet. venv est intégré ; les alternatives incluent virtualenv, conda et uv. Épinglez les versions dans requirements.txt pour la reproductibilité. N'installez jamais de packages globalement avec --user ou en tant que root — utilisez un venv.

python
# Create a virtual environment
# $ python -m venv venv

# Activate it
# Windows: venv\Scripts\activate
# Unix:    source venv/bin/activate

# Install packages
# $ pip install requests
# $ pip install requests==2.28.0
# $ pip install "requests>=2.25,<3.0"

# Install from requirements file
# $ pip install -r requirements.txt

# requirements.txt example:
# requests==2.31.0
# numpy>=1.21.0
# pandas~=2.0.0  # compatible release

# List installed packages
# $ pip list
# $ pip freeze > requirements.txt

# Uninstall
# $ pip uninstall requests

# Show package info
# $ pip show requests

# Modern alternative: uv (faster)
# $ uv pip install requests
12

Date & Heure

Module datetime

Le module datetime fournit les classes date, time, datetime et timedelta. datetime.now() renvoie l'heure locale ; utilisez datetime.now(timezone.utc) pour UTC. weekday() renvoie 0-6 (Lun-Dim). Utilisez toujours des datetimes conscients du fuseau horaire en production pour éviter l'ambiguïté.

python
from datetime import datetime, date, time, timedelta

# Current date and time
now = datetime.now()          # local time
utc_now = datetime.utcnow()   # UTC (deprecated in 3.12)
utc = datetime.now(timezone.utc)  # preferred

# Current date
today = date.today()

# Create specific date/time
dt = datetime(2024, 1, 15, 10, 30, 0)
d = date(2024, 1, 15)
t = time(10, 30, 0)

# Access components
print(now.year, now.month, now.day)
print(now.hour, now.minute, now.second)
print(now.weekday())  # 0=Monday, 6=Sunday

# From timestamp
ts = 1705315200
dt = datetime.fromtimestamp(ts)

# To timestamp
print(datetime.now().timestamp())

Formatage & Analyse

strftime (string format time) convertit datetime en chaîne ; strptime (string parse time) convertit une chaîne en datetime. Le format ISO 8601 (isoformat/fromisoformat) est le meilleur choix pour stocker les dates — il est non ambigu et triable. Mémorisez les codes courants : %Y %m %d %H %M %S.

python
from datetime import datetime

# Format datetime to string (strftime)
dt = datetime(2024, 1, 15, 10, 30)
print(dt.strftime("%Y-%m-%d"))         # 2024-01-15
print(dt.strftime("%Y/%m/%d %H:%M"))   # 2024/01/15 10:30
print(dt.strftime("%B %d, %Y"))        # January 15, 2024
print(dt.strftime("%A"))               # Monday

# Parse string to datetime (strptime)
dt = datetime.strptime("2024-01-15", "%Y-%m-%d")
dt = datetime.strptime("15/01/2024 10:30", "%d/%m/%Y %H:%M")

# Common format codes:
# %Y  year (2024)     %m  month (01)
# %d  day (15)        %H  hour (14)
# %M  minute (30)     %S  second (00)
# %B  month name      %b  month abbrev
# %A  weekday name    %a  weekday abbrev
# %I  12-hour         %p  AM/PM
# %j  day of year     %U  week number

# ISO format (recommended for storage)
iso = dt.isoformat()  # "2024-01-15T10:30:00"
dt = datetime.fromisoformat("2024-01-15T10:30:00")

timedelta & Arithmétique

timedelta représente une durée. Vous pouvez ajouter/soustraire des timedeltas à des datetimes et soustraire deux datetimes pour obtenir un timedelta. timedelta normalise : days=1, hours=25 devient days=2, hours=1. total_seconds() donne la durée entière en secondes.

python
from datetime import datetime, timedelta

now = datetime.now()

# Add/subtract time
tomorrow = now + timedelta(days=1)
last_week = now - timedelta(weeks=1)
in_2_hours = now + timedelta(hours=2)
in_90_days = now + timedelta(days=90)

# Difference between dates
date1 = datetime(2024, 1, 15)
date2 = datetime(2024, 6, 18)
diff = date2 - date1
print(diff.days)         # 155
print(diff.total_seconds())

# timedelta components
td = timedelta(days=5, hours=3, minutes=30)
print(td.days)           # 5
print(td.seconds)        # 12600 (3h 30m in seconds)
print(td.total_seconds())

# Comparisons
if now > date1:
    print("now is later")

# Business day calculation (using numpy)
# import numpy as np
# business_days = np.busday_count(date1.date(), date2.date())

Fuseaux horaires

Utilisez toujours des datetimes conscients du fuseau horaire (Python 3.9+ ZoneInfo est préféré à pytz). Stockez les dates en UTC et convertissez en heure locale uniquement pour l'affichage. Les datetimes naïfs (sans tzinfo) causent des bugs subtils. ZoneInfo utilise la base de données de fuseaux horaires IANA, gérant l'heure d'été automatiquement.

python
from datetime import datetime, timezone, timedelta

# Timezone-aware datetime
utc_time = datetime.now(timezone.utc)
print(utc_time)  # 2024-01-15 10:30:00+00:00

# Create a timezone (offset-based)
tz_ny = timezone(timedelta(hours=-5), "EST")
ny_time = datetime.now(tz_ny)

# Convert between timezones
utc_time = datetime.now(timezone.utc)
ny_time = utc_time.astimezone(timezone(timedelta(hours=-5)))
tokyo_time = utc_time.astimezone(timezone(timedelta(hours=9)))

# Use zoneinfo (Python 3.9+) for IANA timezones
from zoneinfo import ZoneInfo
tz = ZoneInfo("America/New_York")
dt = datetime.now(tz)
print(dt.tzname())  # EST or EDT

# Common IANA timezones:
# "UTC"
# "America/New_York", "America/Los_Angeles"
# "Europe/London", "Europe/Paris"
# "Asia/Tokyo", "Asia/Shanghai"
# "Australia/Sydney"

# Best practice: store UTC, convert for display
utc_stored = datetime.now(timezone.utc)
local_display = utc_stored.astimezone(ZoneInfo("Asia/Shanghai"))
13

Expressions régulières

Bases du module re

re.search() trouve la première correspondance n'importe où ; re.match() seulement au début ; re.fullmatch() exige que toute la chaîne corresponde. Utilisez des chaînes brutes (r'...') pour les motifs afin d'éviter les problèmes d'échappement de barre oblique inverse. Les objets Match fournissent group(), start(), end() et span().

python
import re

# re.search - find first match anywhere in string
m = re.search(r"\d{4}", "Order #2024 was placed")
if m:
    print(m.group())  # 2024
    print(m.start(), m.end())  # 9 13

# re.match - match at beginning of string
m = re.match(r"Hello", "Hello, World")
print(m.group())  # Hello

# re.fullmatch - entire string must match
m = re.fullmatch(r"\d+", "12345")
print(bool(m))  # True

# re.findall - all matches as list
emails = re.findall(r"\S+@\S+", text)
numbers = re.findall(r"\d+", "a1b22c333")  # ['1', '22', '333']

# re.finditer - all matches as iterator (with positions)
for m in re.finditer(r"\w+", "Hello World"):
    print(m.group(), m.span())

# Match object methods
m = re.search(r"(\w+)@(\w+)", "[email protected]")
print(m.group())   # user@domain (whole match)
print(m.group(1))  # user (first group)
print(m.group(2))  # domain (second group)
print(m.groups())  # ('user', 'domain')

Syntaxe des motifs

Syntaxe regex : [] pour les classes de caractères, \d \w \s pour les ensembles courants, quantificateurs (* + ? {}) pour la répétition, ^ $ \b pour les ancres, () pour les groupes, | pour l'alternance. Utilisez des chaînes brutes (r'...') pour que les barres obliques inverses soient littérales. Les quantificateurs gourmands correspondent au maximum ; ajoutez ? pour paresseux (ex. *?).

python
import re

# Character classes
re.findall(r"[aeiou]", "hello")      # vowels
re.findall(r"[^aeiou]", "hello")     # non-vowels
re.findall(r"[a-z]", "Hello123")     # lowercase
re.findall(r"[A-Za-z0-9]", "Hi-1!")  # alphanumeric

# Predefined classes
# .   any char except newline
# \d  digit [0-9]      \D  non-digit
# \w  word char [a-zA-Z0-9_]  \W  non-word
# \s  whitespace       \S  non-whitespace

# Quantifiers
# *     0 or more
# +     1 or more
# ?     0 or 1
# {n}   exactly n
# {n,}  n or more
# {n,m} between n and m
re.findall(r"\d{3}", "1234567")      # ['123', '456']
re.findall(r"\d{2,4}", "12345678")   # ['1234', '5678']

# Anchors
# ^  start of string    $  end of string
# \b word boundary
re.findall(r"^\w+", "Hello World")   # ['Hello']
re.findall(r"\b\w+\b", "hi there") # ['hi', 'there']

# Groups & alternation
re.findall(r"(cat|dog)", "cat and dog")  # ['cat', 'dog']
re.findall(r"(\w+)@(\w+\.\w+)", "[email protected]")

Substitution & Division

re.sub() remplace les correspondances — utilisez des références arrière (\1, \2) pour référencer les groupes, ou une fonction pour un remplacement dynamique. re.split() est plus puissant que str.split() — il accepte des motifs regex. Les groupes de capture dans le motif sont inclus dans le résultat. Utilisez re.subn() pour obtenir (résultat, nombre).

python
import re

# re.sub - replace matches
result = re.sub(r"\d+", "#", "a1b22c333")
# 'a#b#c#'

# Replace with count limit
result = re.sub(r"\d", "X", "a1b2c3", count=2)
# 'aXbXc3'

# Use backreferences in replacement
result = re.sub(r"(\w+)@(\w+)", r"\2.\1", "user@domain")
# 'domain.user'

# Use function as replacement
def upper(m):
    return m.group().upper()

result = re.sub(r"\b[a-z]", upper, "hello world")
# 'Hello World' (capitalize first letter of each word)

# re.split - split by pattern
parts = re.split(r"[,;\s]+", "a, b; c d")
# ['a', 'b', 'c', 'd']

# Split with capture groups (keeps delimiters)
parts = re.split(r"([,;])", "a,b;c")
# ['a', ',', 'b', ';', 'c']

# Split with maxsplit
parts = re.split(r",", "a,b,c,d", maxsplit=2)
# ['a', 'b', 'c,d']

Compilation & Indicateurs

Compilez les motifs avec re.compile() lorsque vous les utilisez plusieurs fois — c'est plus rapide. Les indicateurs modifient le comportement : IGNORECASE, MULTILINE, DOTALL, VERBOSE (autorise les commentaires/espaces dans les motifs). Les groupes nommés (?P<name>...) améliorent la lisibilité. Les lookahead (?=) et lookbehind (?<=) correspondent sans consommer.

python
import re

# Compile pattern for reuse (faster when used many times)
email_re = re.compile(r"^[\w.+-]+@([\w-]+\.)+[\w-]+$")
print(email_re.match("[email protected]"))  # match object
print(email_re.match("invalid"))           # None

# Common flags
re.IGNORECASE  # case-insensitive
re.MULTILINE   # ^ and $ match line boundaries
re.DOTALL      # . matches newline too
re.VERBOSE     # allow whitespace & comments in pattern

# Combine flags with |
pattern = re.compile(r"""
    ^           # start of line
    (\w+)      # capture word
    \s+        # whitespace
    (\d+)      # capture number
""", re.VERBOSE | re.MULTILINE)

# Named groups (more readable)
m = re.match(r"(?P<year>\d{4})-(?P<month>\d{2})", "2024-01")
print(m.group("year"))   # 2024
print(m.group("month"))  # 01
print(m.groupdict())     # {'year': '2024', 'month': '01'}

# Lookahead/lookbehind
re.findall(r"\d+(?= dollars)", "100 dollars, 200 euros")
# ['100']  (positive lookahead)
re.findall(r"(?<=\$)\d+", "$100 and $200")
# ['100', '200']  (positive lookbehind)
14

Async & Concurrence

Bases d'asyncio

asyncio est le framework d'E/S asynchrone de Python. 'async def' définit une coroutine ; 'await' suspend jusqu'à ce qu'un résultat soit prêt. asyncio.run() démarre la boucle d'événements. asyncio.gather() exécute des coroutines de manière concurrente. Les coroutines permettent une E/S à haute concurrence sans threads.

python
import asyncio

# Define a coroutine
async def greet(name, delay):
    await asyncio.sleep(delay)  # non-blocking sleep
    return f"Hello, {name}!"

# Run a coroutine
async def main():
    result = await greet("Alice", 1)
    print(result)

# Run the event loop
asyncio.run(main())

# Concurrent execution with gather
async def main():
    # Run coroutines concurrently
    results = await asyncio.gather(
        greet("Alice", 2),
        greet("Bob", 1),
        greet("Carol", 3)
    )
    print(results)  # all complete after 3 seconds (max delay)

asyncio.run(main())

# asyncio.create_task - schedule without awaiting immediately
async def main():
    task = asyncio.create_task(greet("Alice", 1))
    # do other work here
    result = await task  # await when needed
    print(result)

HTTP async & Délais

asyncio.timeout() (Python 3.11+) annule les opérations qui prennent trop de temps. Pour HTTP, utilisez aiohttp (async) au lieu de requests (sync). asyncio.Queue permet des motifs producteur-consommateur. Async est idéal pour le travail lié aux E/S (réseau, disque) — pas pour le travail lié au CPU.

python
import asyncio

# Async timeout
async def fetch_with_timeout(url, timeout=5):
    try:
        async with asyncio.timeout(timeout):
            # simulate async operation
            await asyncio.sleep(2)
            return f"Data from {url}"
    except asyncio.TimeoutError:
        return "Request timed out"

# Using aiohttp (third-party) for HTTP
# import aiohttp
# 
# async def fetch(url):
#     async with aiohttp.ClientSession() as session:
#         async with session.get(url) as response:
#             return await response.text()

# Producer-consumer pattern
async def producer(queue):
    for i in range(5):
        await asyncio.sleep(0.1)
        await queue.put(f"item-{i}")
    await queue.put(None)  # sentinel

async def consumer(queue):
    while True:
        item = await queue.get()
        if item is None:
            break
        print(f"Processed: {item}")
        queue.task_done()

async def main():
    queue = asyncio.Queue()
    await asyncio.gather(producer(queue), consumer(queue))

asyncio.run(main())

Threading

Le threading est pour la concurrence liée aux E/S (réseau, E/S fichier). Le GIL de Python empêche la véritable exécution CPU parallèle dans les threads. Utilisez Lock pour protéger l'état partagé contre les conditions de course. Pour le travail lié au CPU, utilisez multiprocessing à la place. Les threads partagent la mémoire ; les processus non.

python
import threading
import time

# Basic threading
def worker(name, delay):
    print(f"Worker {name} starting")
    time.sleep(delay)
    print(f"Worker {name} done")

# Create and start threads
t1 = threading.Thread(target=worker, args=("A", 2))
t2 = threading.Thread(target=worker, args=("B", 1))
t1.start()
t2.start()

# Wait for threads to complete
t1.join()
t2.join()
print("All done")

# Thread with Lock (for shared state)
counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(100000):
        with lock:  # acquire/release lock
            counter += 1

threads = [threading.Thread(target=increment) for _ in range(5)]
for t in threads: t.start()
for t in threads: t.join()
print(f"Counter: {counter}")  # 500000 (correct with lock)

Multiprocessing

Le multiprocessing contourne le GIL pour un véritable parallélisme CPU — chaque processus a son propre interpréteur Python. Utilisez Pool pour des opérations map parallèles. concurrent.futures fournit une API unifiée pour les threads et les processus. Protégez toujours avec if __name__ == '__main__' sur Windows.

python
from multiprocessing import Process, Pool, Queue
import os

# Basic process
def worker(name):
    print(f"Process {name} PID: {os.getpid()}")

if __name__ == "__main__":
    p = Process(target=worker, args=("A",))
    p.start()
    p.join()

# Process pool for parallel work
def square(x):
    return x * x

if __name__ == "__main__":
    with Pool(4) as pool:  # 4 worker processes
        results = pool.map(square, range(10))
        print(results)  # [0, 1, 4, 9, ..., 81]
    
    # Asynchronous map
    with Pool(4) as pool:
        result = pool.map_async(square, range(10))
        print(result.get())  # blocks until done

# concurrent.futures (higher-level API)
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor

with ProcessPoolExecutor() as executor:
    results = list(executor.map(square, range(10)))

with ThreadPoolExecutor() as executor:
    futures = [executor.submit(square, i) for i in range(10)]
    results = [f.result() for f in futures]
15

Décorateurs

Décorateur de base

Les décorateurs enveloppent une fonction pour étendre ou modifier son comportement sans changer la source originale. La syntaxe @ est du sucre syntaxique pour affecter le résultat de l'appel du décorateur au nom de la fonction. Utilisez *args, **kwargs dans le wrapper pour qu'il fonctionne avec toute signature.

python
# A decorator is a function that takes a function and returns a new function
def uppercase_result(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

@uppercase_result
def greet(name):
    return f"hello, {name}"

print(greet("world"))  # HELLO, WORLD
# @uppercase_result is sugar for: greet = uppercase_result(greet)

functools.wraps (Préserver les métadonnées)

Sans @wraps, la fonction enveloppée perd son __name__, __doc__ et sa signature originaux — les outils de débogage et help() affichent 'wrapper' à la place. Utilisez toujours @functools.wraps(func) dans les décorateurs pour préserver les métadonnées. C'est une meilleure pratique quasi universelle.

python
from functools import wraps

def log_calls(func):
    @wraps(func)  # copies __name__, __doc__, __module__
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}({args}, {kwargs})")
        return func(*args, **kwargs)
    return wrapper

@log_calls
def add(a, b):
    """Add two numbers."""
    return a + b

print(add.__name__)  # 'add' (not 'wrapper')
print(add.__doc__)   # 'Add two numbers.'
help(add)            # shows original docstring

Décorateur avec arguments

Lorsqu'un décorateur prend des arguments, vous avez besoin de trois niveaux d'imbrication : la fabrique (prend les args), le décorateur (prend la fonction), et le wrapper (prend les args d'appel). @repeat(3) appelle repeat(3) en premier, qui renvoie le décorateur, qui est ensuite appliqué à la fonction.

python
# A decorator factory: returns the actual decorator
def repeat(times):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def say_hi(name):
    print(f"Hi, {name}!")

say_hi("Alice")
# Hi, Alice! (printed 3 times)

Décorateur basé sur une classe

Les décorateurs de classe utilisent __init__ pour stocker la fonction et __call__ pour intercepter les invocations. Ils sont idéaux lorsque le décorateur doit maintenir un état (comme un compteur d'appels ou un cache). L'instance de classe remplace la fonction, donc l'appeler déclenche __call__.

python
class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0
        wraps(func)(self)  # preserve metadata

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"{self.func.__name__} called {self.count} times")
        return self.func(*args, **kwargs)

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

say_hello()  # count=1
say_hello()  # count=2
say_hello()  # count=3
print(say_hello.count)  # 3

Décorateurs intégrés (@property, @staticmethod, @classmethod)

@property transforme une méthode en attribut calculé (accessible sans parenthèses). @classmethod reçoit la classe comme premier argument — parfait pour les constructeurs alternatifs. @staticmethod ne reçoit aucun premier argument implicite — juste une fonction qui se trouve vivre dans l'espace de noms de la classe. Ensemble, ils forment l'épine dorsale de la POO pythonique.

python
class Circle:
    pi = 3.14159

    def __init__(self, radius):
        self._radius = radius

    @property
    def area(self):
        return Circle.pi * self._radius ** 2

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value

    @classmethod
    def from_diameter(cls, diameter):
        return cls(diameter / 2)

    @staticmethod
    def is_valid_radius(r):
        return r >= 0

c = Circle(5)
print(c.area)              # 78.54 (no parentheses!)
c.radius = 10             # uses the setter
c2 = Circle.from_diameter(20)  # alternative constructor

Décorateurs empilés

Lors de l'empilement de décorateurs, ils sont appliqués de bas en haut (celui le plus proche de la fonction s'exécute en premier) mais s'exécutent de haut en bas au moment de l'appel. Donc @bold enveloppe @italic qui enveloppe greet. Le résultat est imbriqué comme des couches d'oignon. L'ordre compte — les inverser change l'imbrication de sortie.

python
from functools import wraps

def bold(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return f"<b>{func(*args, **kwargs)}</b>"
    return wrapper

def italic(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return f"<i>{func(*args, **kwargs)}</i>"
    return wrapper

@bold
@italic
def greet(name):
    return f"Hello, {name}"

print(greet("World"))
# <b><i>Hello, World</i></b>
# Applied bottom-up: italic first, then bold
16

Générateurs & Itérateurs

Fonctions génératrices (yield)

Les générateurs produisent des valeurs paresseusement avec yield — ils suspendent l'exécution après chaque yield et reprennent lorsque next() est appelé. Cela les rend efficaces en mémoire pour les séquences grandes ou infinies puisqu'une seule valeur existe en mémoire à la fois. Une fois épuisé, un générateur ne peut pas être réutilisé.

python
# A generator uses 'yield' to produce values lazily, one at a time
def count_up_to(n):
    count = 1
    while count <= n:
        yield count
        count += 1

gen = count_up_to(5)
print(next(gen))  # 1
print(next(gen))  # 2
print(list(gen))  # [3, 4, 5] (exhausts the rest)

# Generators are memory-efficient: they don't build the whole list
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
print([next(fib) for _ in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Expressions génératrices

Les expressions génératrices sont l'équivalent paresseux des compréhensions de liste — utilisez des parenthèses au lieu de crochets. Elles utilisent une mémoire constante quelle que soit la taille, ce qui les rend idéales pour sum(), max(), any(), ou pour alimenter d'autres itérateurs. Préférez-les aux compréhensions de liste lorsque vous n'avez pas besoin d'accès aléatoire.

python
# Like list comprehensions, but lazy (uses parentheses)
squares_list = [x ** 2 for x in range(10)]      # builds full list
squares_gen  = (x ** 2 for x in range(10))      # lazy generator

print(squares_gen)        # <generator object>
print(next(squares_gen))  # 0
print(next(squares_gen))  # 1

# Memory comparison
import sys
print(sys.getsizeof([x for x in range(10000)]))  # ~87616 bytes
print(sys.getsizeof((x for x in range(10000))))  # ~200 bytes (constant!)

# Use in sum(), list(), any() etc.
total = sum(x ** 2 for x in range(100))  # no extra list created
print(total)  # 328350

Protocole itérateur (__iter__, __next__)

Le protocole itérateur exige __iter__ (renvoie un itérateur) et __next__ (renvoie la valeur suivante ou lève StopIteration). Les itérables peuvent être parcourus ; les itérateurs produisent des valeurs une à la fois. Pour les itérables réutilisables, séparez l'itérable (renvoie un nouvel itérateur) de l'itérateur (détient l'état).

python
class Range2:
    """A custom iterator that yields even numbers."""
    def __init__(self, start, end):
        self.current = start
        self.end = end

    def __iter__(self):
        return self  # the object is its own iterator

    def __next__(self):
        if self.current >= self.end:
            raise StopIteration
        value = self.current
        self.current += 2
        return value

r = Range2(0, 10)
for num in r:
    print(num)  # 0, 2, 4, 6, 8

# An iterable returns a fresh iterator each time __iter__ is called.
# An iterator returns itself and raises StopIteration when exhausted.

send(), throw(), close()

Les méthodes avancées des générateurs permettent une communication bidirectionnelle : send() passe une valeur dans le générateur (devient le résultat de yield), throw() injecte une exception au point de yield, et close() termine le générateur. Vous devez 'amorcer' le générateur avec next() avant d'envoyer. Elles alimentent les coroutines et les frameworks async.

python
def echo():
    while True:
        received = yield  # yield without a value, receives via send()
        print(f"Echo: {received}")

gen = echo()
next(gen)          # prime the generator (advance to first yield)
gen.send("hello")  # Echo: hello
gen.send("world")  # Echo: world

# throw() injects an exception at the yield point
def safe_gen():
    try:
        while True:
            yield
    except ValueError:
        print("Caught ValueError inside generator")

g = safe_gen()
next(g)
g.throw(ValueError, "boom")  # Caught ValueError inside generator

# close() stops the generator (raises GeneratorExit)
gen.close()

Pipelines de générateurs

Les pipelines de générateurs chaînent des producteurs paresseux afin que les données circulent par étapes, un élément à la fois — chaque élément est entièrement traité avant que le suivant ne soit lu. Cela évite de construire des listes intermédiaires et est la fondation du traitement de flux de données. Les pipes Unix fonctionnent conceptuellement de la même manière.

python
# Chain generators to build data-processing pipelines
def numbers():
    for i in range(1, 11):
        yield i

def squared(seq):
    for n in seq:
        yield n ** 2

def evens(seq):
    for n in seq:
        if n % 2 == 0:
            yield n

# Each stage processes one item at a time — no intermediate lists
pipeline = evens(squared(numbers()))
print(list(pipeline))  # [4, 16, 36, 64, 100]

# Equivalent with generator expressions:
result = (n for n in (x ** 2 for x in range(1, 11)) if n % 2 == 0)
print(list(result))  # [4, 16, 36, 64, 100]

yield from (Délégation)

yield from délègue tous les yields (et send/throw/close) à un sous-itérateur, aplatissant les structures imbriquées et composant les coroutines. Il est particulièrement puissant pour les générateurs récursifs — le cas d'usage classique est l'aplatissage de listes arbitrairement imbriquées. Dans le code async, 'await' est construit sur le même concept.

python
# yield from delegates to a sub-iterator (Python 3.3+)
def flatten(nested):
    for item in nested:
        if isinstance(item, (list, tuple)):
            yield from flatten(item)  # recursive delegation
        else:
            yield item

data = [1, [2, 3, [4, 5]], 6, [7, [8, [9]]]]
print(list(flatten(data)))
# [1, 2, 3, 4, 5, 6, 7, 8, 9]

# yield from also forwards send()/throw() to the sub-generator,
# making it essential for coroutine composition.
17

Gestionnaires de contexte

Bases de l'instruction with

L'instruction with garantit que les ressources sont libérées (fichiers fermés, verrous relâchés, connexions renvoyées) même lorsque des exceptions se produisent. Elle appelle __enter__ au début et __exit__ à la fin. Préférez toujours 'with' au try/finally manuel pour la gestion des ressources — c'est plus sûr et plus lisible.

python
# 'with' guarantees cleanup even if an exception occurs
with open("data.txt", "r") as f:
    content = f.read()
# f.close() is called automatically here, even if read() raised

# Without 'with' you must manually close:
f = open("data.txt", "r")
try:
    content = f.read()
finally:
    f.close()  # easy to forget!

# Common built-in context managers:
with open("out.txt", "w") as f, open("in.txt") as g:
    f.write(g.read())  # both files close automatically

Gestionnaire de contexte personnalisé (Classe)

Un gestionnaire de contexte basé sur une classe implémente __enter__ (configuration, renvoie l'objet de contexte) et __exit__(exc_type, exc_val, exc_tb) (nettoyage). Les arguments __exit__ reçoivent les informations d'exception si une s'est produite ; renvoyer True la supprime. Ce motif est idéal pour la configuration/démontage complexes comme les transactions de base de données.

python
class Timer:
    def __init__(self, label="Timer"):
        self.label = label

    def __enter__(self):
        import time
        self.start = time.perf_counter()
        return self  # value bound to 'as' variable

    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        elapsed = time.perf_counter() - self.start
        print(f"{self.label}: {elapsed:.4f}s")
        # Return False (or None) to propagate exceptions
        # Return True to suppress the exception
        return False

with Timer("Processing"):
    total = sum(i ** 2 for i in range(1_000_000))
# Processing: 0.1234s

contextlib.contextmanager

contextlib.contextmanager transforme une fonction génératrice en gestionnaire de contexte — le code avant yield est __enter__, le code après yield (dans finally) est __exit__. C'est plus concis qu'une classe pour les cas simples. Yield une valeur pour la fournir à la variable 'as'. Utilisez try/finally pour garantir le nettoyage.

python
from contextlib import contextmanager
import time

@contextmanager
def timer(label="Timer"):
    start = time.perf_counter()
    try:
        yield  # code inside the 'with' block runs here
    finally:
        elapsed = time.perf_counter() - start
        print(f"{label}: {elapsed:.4f}s")

with timer("My task"):
    sum(i ** 2 for i in range(1_000_000))
# My task: 0.1234s

# You can also yield a value to bind with 'as'
@contextmanager
def open_db(path):
    db = connect(path)
    try:
        yield db
    finally:
        db.close()

with open_db("app.db") as db:
    db.query("SELECT 1")

Gestionnaires de contexte multiples

Python 3.10+ permet des instructions 'with' multi-lignes parenthésées pour une syntaxe plus propre. Pour des nombres dynamiques de gestionnaires de contexte, contextlib.ExitStack les gère en groupe et les déroule tous dans l'ordre inverse. ExitStack est essentiel lorsque le nombre de ressources n'est pas connu avant l'exécution.

python
# Python 3.10+ supports parenthesized context managers
with (
    open("input.txt") as fin,
    open("output.txt", "w") as fout,
):
    fout.write(fin.read())

# Pre-3.10: nest them or use contextlib.ExitStack
from contextlib import ExitStack

files = ["a.txt", "b.txt", "c.txt"]
with ExitStack() as stack:
    handles = [stack.enter_context(open(f)) for f in files]
    for h in handles:
        print(h.read())

Utilitaires contextlib (suppress, redirect)

contextlib.suppress remplace try/except/pass pour les exceptions attendues — beaucoup plus lisible. redirect_stdout/redirect_stderr capturent la sortie qui irait autrement à la console, utile pour les tests ou la journalisation. Ces utilitaires évitent le code passe-partout et rendent l'intention explicite.

python
from contextlib import suppress, redirect_stdout, redirect_stderr
import io, warnings

# suppress: ignore specific exceptions (cleaner than try/except/pass)
with suppress(FileNotFoundError):
    os.remove("temp.txt")  # no error if file doesn't exist

# redirect_stdout: capture print output
buffer = io.StringIO()
with redirect_stdout(buffer):
    print("This goes to the buffer, not console")
captured = buffer.getvalue()

# redirect_stderr: capture error/warning output
err_buf = io.StringIO()
with redirect_stderr(err_buf):
    warnings.warn("a warning")
print(err_buf.getvalue())  # the warning text

# Also: contextlib.chdir (3.11+) to temporarily change directory
# from contextlib import chdir
# with chdir("/tmp"): ...

Gestionnaires de contexte async

Les gestionnaires de contexte async utilisent __aenter__/__aexit__ (notez le préfixe 'a') et l'instruction 'async with'. Ils sont essentiels pour gérer les ressources async comme les connexions de base de données ou les sessions HTTP (ex. aiohttp.ClientSession). Le nettoyage s'exécute même si un await ou une exception se produit dans le bloc.

python
import asyncio

class AsyncDB:
    async def __aenter__(self):
        print("connecting...")
        await asyncio.sleep(0.1)  # simulate async connect
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        print("closing...")
        await asyncio.sleep(0.1)  # simulate async close
        return False

    async def query(self, sql):
        return f"result of: {sql}"

async def main():
    async with AsyncDB() as db:
        result = await db.query("SELECT 1")
        print(result)

asyncio.run(main())
# connecting...
# result of: SELECT 1
# closing...
18

Indications de type

Annotations de base des variables & fonctions

Les indications de type documentent les types attendus mais ne sont PAS appliquées à l'exécution — Python reste dynamiquement typé. Utilisez un vérificateur statique comme mypy ou pyright pour détecter les erreurs de type avant l'exécution. Les génériques intégrés (list[str], dict[str, int]) nécessitent Python 3.9+ ; les versions antérieures ont besoin de typing.List, typing.Dict.

python
# Variable annotations (Python 3.6+)
name: str = "Alice"
age: int = 30
scores: list[float] = [95.5, 88.0, 92.3]
config: dict[str, int] = {"timeout": 30}

# Function annotations
def greet(name: str, excited: bool = False) -> str:
    punctuation = "!" if excited else "."
    return f"Hello, {name}{punctuation}"

print(greet("World", excited=True))  # Hello, World!

# Annotations are optional and NOT enforced at runtime
def add(a: int, b: int) -> int:
    return a + b
add("2", "3")  # runs fine, returns "23" (no error!)

Module typing (List, Dict, Tuple, Optional)

Le module typing fournit des alias génériques pour les anciennes versions de Python. Depuis 3.9, vous pouvez utiliser les types intégrés directement (list[str] au lieu de List[str]). Optional[X] est un raccourci pour Union[X, None] — utilisez-le pour signaler qu'une fonction peut renvoyer None, forçant les appelants à gérer le cas None.

python
from typing import List, Dict, Tuple, Set, FrozenSet

# Pre-3.9 style (still works, needed for older Python)
names: List[str] = ["Alice", "Bob"]
scores: Dict[str, int] = {"Alice": 95}
point: Tuple[int, int] = (10, 20)
mixed: Tuple[str, int, float] = ("a", 1, 2.0)
variadic: Tuple[int, ...] = (1, 2, 3)  # variable-length

# Python 3.9+ built-in generics (preferred)
names: list[str] = ["Alice", "Bob"]
scores: dict[str, int] = {"Alice": 95}
point: tuple[int, int] = (10, 20)

# Optional means "could be None"
from typing import Optional
def find_user(uid: int) -> Optional[str]:
    if uid == 1:
        return "Alice"
    return None  # could also just 'return'

Types Union & Literal

Union[X, Y] (ou X | Y en 3.10+) signifie qu'une valeur peut être de l'un ou l'autre type. Literal restreint une valeur à des constantes spécifiques — excellent pour les énumérations de chaînes sans la surcharge d'enum, et pour la distribution de fonctions surchargées. mypy utilise Literal pour restreindre les types et détecter les arguments invalides au moment de la vérification.

python
from typing import Union, Literal, overload

# Union: value can be one of several types
def process(data: Union[str, bytes]) -> str:
    if isinstance(data, bytes):
        return data.decode("utf-8")
    return data

# Python 3.10+ union syntax with | (preferred)
def process2(data: str | bytes) -> str:
    if isinstance(data, bytes):
        return data.decode("utf-8")
    return data

# Literal: restrict to specific constant values
def set_mode(mode: Literal["r", "w", "a"]) -> None:
    print(f"Mode set to {mode}")

set_mode("r")   # OK
# set_mode("x")  # mypy error: not a valid literal

# Literal for boolean-like flags
Direction = Literal["up", "down", "left", "right"]

TypeVar & Génériques

TypeVar crée des variables de type génériques afin que les fonctions et classes puissent préserver les relations de type (ex. 'renvoie le même type que l'entrée'). Utilisez bound= pour contraindre à un sous-type, ou spécifiez des contraintes comme TypeVar('T', int, float). Les classes génériques utilisent Generic[T] comme base pour devenir des conteneurs paramétrés.

python
from typing import TypeVar, Generic, List

T = TypeVar("T")  # a generic type variable

def first(items: List[T]) -> T:
    return items[0]

# Type inference: T is bound to the argument's type
x: int = first([1, 2, 3])        # T = int
y: str = first(["a", "b", "c"])  # T = str

# Bounded TypeVar: T must be a subtype of Number
from typing import TypeVar
from numbers import Number
N = TypeVar("N", bound=Number)

def sum_all(values: list[N]) -> N:
    total = values[0]
    for v in values[1:]:
        total = total + v
    return total

# Generic class
class Stack(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []
    def push(self, item: T) -> None:
        self._items.append(item)
    def pop(self) -> T:
        return self._items.pop()

s: Stack[int] = Stack()
s.push(1)
# s.push("x")  # mypy error

Callable, Alias de type & Protocoles

Callable[[int, str], bool] décrit une fonction prenant int et str, renvoyant bool. Les alias de type donnent des noms descriptifs aux types complexes. Protocol permet le typage structurel (canard) — tout objet avec les bonnes méthodes satisfait le protocole, aucune héritage requis. C'est la réponse de Python aux interfaces.

python
from typing import Callable, Protocol, TypeAlias

# Callable signature: Callable[[ArgTypes], ReturnType]
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
    return func(a, b)

apply(lambda x, y: x + y, 3, 4)  # 7

# Type aliases (3.12+ uses 'type' statement; older uses assignment)
type Vector = list[float]          # 3.12+
Vector2: TypeAlias = list[float]   # 3.10+

def magnitude(v: Vector) -> float:
    return sum(x ** 2 for x in v) ** 0.5

# Protocol: structural typing (duck typing with static checks)
class Drawable(Protocol):
    def draw(self) -> None: ...

def render(obj: Drawable) -> None:
    obj.draw()  # any object with a draw() method works

class Circle:
    def draw(self) -> None:
        print("drawing circle")

render(Circle())  # OK — Circle has draw()

Vérification de type avec mypy

mypy est le vérificateur de type statique le plus populaire pour Python — il analyse les indications de type sans exécuter le code. Il détecte les bugs de gestion None, les mauvais types d'arguments et les retours manquants. Commencez par le typage progressif : ajoutez des indications au nouveau code et exécutez mypy dans CI. Utilisez --strict pour les nouveaux projets afin d'imposer des annotations complètes.

python
# Save as example.py, then run:  mypy example.py
from typing import Optional

def divide(a: float, b: float) -> Optional[float]:
    if b == 0:
        return None
    return a / b

result = divide(10, 0)
# Without checking, this would crash at runtime:
# print(result + 1)  # TypeError: NoneType + int

# mypy catches it:
# error: Unsupported operand types for + ("None" and "int")
# fix: check for None first
if result is not None:
    print(result + 1)

# Run strict mode for maximum safety:
#   mypy --strict example.py
# Common strict flags: --disallow-untyped-defs, --no-implicit-optional
19

Data Classes

@dataclass de base

@dataclass génère automatiquement __init__, __repr__ et __eq__ basés sur les champs annotés — éliminant le passe-partout pour les classes de données. Idéal pour les objets de valeur, configs, DTO et enregistrements. Disponible depuis Python 3.7. Les champs doivent avoir des annotations de type ; l'annotation définit le champ.

python
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

p1 = Point(1.0, 2.0)
p2 = Point(1.0, 2.0)

print(p1)         # Point(x=1.0, y=2.0)  — auto __repr__
print(p1 == p2)   # True  — auto __eq__ (compares fields)
print(p1.x)       # 1.0

# Without @dataclass you'd write all this boilerplate:
# class Point:
#     def __init__(self, x, y): self.x = x; self.y = y
#     def __repr__(self): ...
#     def __eq__(self, other): ...

Valeurs par défaut & default_factory

Les valeurs par défaut mutables (listes, dicts, sets) doivent utiliser field(default_factory=list) — utiliser [] directement partagerait une liste entre toutes les instances, un bug classique. default_factory est appelé une fois par instance pour créer un objet frais. Les valeurs par défaut immuables simples (int, str, bool, None) peuvent être affectées directement.

python
from dataclasses import dataclass, field

@dataclass
class Student:
    name: str
    grade: str = "A"          # simple default
    tags: list[str] = field(default_factory=list)  # mutable default!
    scores: dict[str, int] = field(default_factory=dict)

s = Student("Alice")
print(s)  # Student(name='Alice', grade='A', tags=[], scores={})

s.tags.append("honors")
s2 = Student("Bob")
print(s2.tags)  # [] — each instance gets its own list

# NEVER use [] or {} as a direct default — all instances would share
# the same mutable object (classic Python pitfall).

frozen, eq & order

frozen=True rend une dataclass immuable — les champs ne peuvent pas être réaffectés, et l'instance devient hachable (utilisable comme clés de dict ou membres de set). order=True ajoute des méthodes de comparaison pour le tri. Combinez frozen=True avec order=True pour des types de valeur immuables et triables comme les coordonnées, couleurs ou versions.

python
from dataclasses import dataclass

# frozen=True makes instances immutable (hashable, usable as dict keys)
@dataclass(frozen=True)
class Color:
    r: int
    g: int
    b: int

c = Color(255, 0, 0)
# c.r = 128  # FrozenInstanceError!
print(hash(c))  # works — frozen dataclasses are hashable

# order=True generates __lt__, __le__, __gt__, __ge__ for sorting
@dataclass(order=True)
class Priority:
    level: int

tasks = [Priority(3), Priority(1), Priority(2)]
tasks.sort()
print(tasks)  # [Priority(level=1), Priority(level=2), Priority(level=3)]

# Common combo: frozen + order for immutable comparable values
@dataclass(frozen=True, order=True)
class Version:
    major: int
    minor: int

__post_init__ & personnalisation des champs

__post_init__ s'exécute automatiquement après le __init__ généré — utilisez-le pour calculer les champs dérivés, valider les valeurs ou effectuer la configuration. field(init=False) crée un champ absent du constructeur (bon pour les valeurs calculées/mises en cache). field(repr=False, compare=False) masque les champs de repr et des vérifications d'égalité.

python
from dataclasses import dataclass, field

@dataclass
class User:
    email: str
    _email_normalized: str = field(init=False, repr=False)
    id: int = field(default=0)

    def __post_init__(self):
        # runs after __init__; compute derived fields here
        self._email_normalized = self.email.strip().lower()

u = User("  [email protected] ")
print(u.email)             # '  [email protected] '
print(u._email_normalized) # '[email protected]'

# field(init=False) excludes a field from __init__
# field(repr=False) hides it from the repr
# field(compare=False) excludes from __eq__/__hash__
# field(metadata={...}) attaches custom metadata

Héritage & slots

Les dataclasses prennent en charge l'héritage — les champs enfants sont ajoutés après les champs parents, et vous pouvez remplacer les valeurs par défaut parentes. Note : un champ avec une valeur par défaut dans le parent ne peut pas être suivi d'un champ sans valeur par défaut dans l'enfant. slots=True (3.10+) empêche l'ajout d'attributs arbitraires et réduit considérablement la mémoire par instance — idéal pour des millions de petits objets.

python
from dataclasses import dataclass

@dataclass
class Animal:
    name: str
    sound: str = "..."

@dataclass
class Dog(Animal):
    breed: str = "unknown"
    sound: str = "Woof"  # override parent default

d = Dog("Rex", breed="Labrador")
print(d)  # Dog(name='Rex', sound='Woof', breed='Labrador')

# Python 3.10+: slots=True saves memory (no __dict__)
@dataclass(slots=True)
class Pixel:
    r: int
    g: int
    b: int

p = Pixel(0, 128, 255)
# p.new_field = 1  # AttributeError — slots prevent arbitrary attrs
# Saves ~40-50% memory vs regular dataclass for many instances
20

Collections & Itertools

namedtuple

namedtuple crée des sous-classes de tuple avec des champs nommés — aussi efficaces en mémoire que les tuples mais beaucoup plus lisibles. Ils sont immuables, utilisez donc _replace() pour créer des copies modifiées. Préférez typing.NamedTuple pour le nouveau code puisqu'il prend en charge les annotations de type et les valeurs par défaut. Excellent pour renvoyer plusieurs valeurs d'une fonction.

python
from collections import namedtuple

# Lightweight immutable class with named fields
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p.x, p.y)       # 3 4  — access by name
print(p[0], p[1])     # 3 4  — also by index
print(p._asdict())    # {'x': 3, 'y': 4}

# More memory-efficient than a full class
# Use _replace to create a modified copy (immutable!)
p2 = p._replace(x=10)
print(p2)  # Point(x=10, y=4)

# Python 3.6+ typing.NamedTuple for type hints:
from typing import NamedTuple
class Point3D(NamedTuple):
    x: float
    y: float
    z: float = 0.0

Counter

Counter est une sous-classe de dict pour compter des objets hachables — parfait pour l'analyse de fréquence, les histogrammes et les votes. most_common(n) renvoie les n premiers éléments. Les clés manquantes renvoient 0 au lieu de lever KeyError. Counter prend en charge +, -, &, | pour l'arithmétique d'ensemble sur les comptes.

python
from collections import Counter

# Count hashable items
words = "the cat sat on the mat the cat".split()
c = Counter(words)
print(c)  # Counter({'the': 3, 'cat': 2, 'sat': 1, 'on': 1, 'mat': 1})

# Most common elements
print(c.most_common(2))  # [('the', 3), ('cat', 2)]

# Arithmetic on counters
c1 = Counter(a=3, b=1)
c2 = Counter(a=1, b=2)
print(c1 + c2)  # Counter({'a': 4, 'b': 3})
print(c1 - c2)  # Counter({'a': 2})  (drops zero/negatives)

# Missing keys return 0 (not KeyError)
print(c["dog"])  # 0

# Update and elements
c.update(["cat", "cat"])
print(sorted(c.elements()))  # ['cat','cat','cat','cat','mat','on','sat','the','the','the']

defaultdict

defaultdict crée automatiquement les clés manquantes avec une valeur par défaut issue d'une fonction de fabrique — list pour le regroupement, int pour le comptage, set pour la déduplication. Cela élimine le passe-partout 'if key not in dict'. La fabrique n'est appelée que lorsqu'une clé manque, pas à chaque accès.

python
from collections import defaultdict

# Group items by key without checking if key exists
words = ["apple", "banana", "avocado", "blueberry", "cherry"]
by_first = defaultdict(list)
for w in words:
    by_first[w[0]].append(w)
print(dict(by_first))
# {'a': ['apple', 'avocado'], 'b': ['banana', 'blueberry'], 'c': ['cherry']}

# Counting with int (default 0)
counts = defaultdict(int)
for w in words:
    counts[w[0]] += 1
print(dict(counts))  # {'a': 2, 'b': 2, 'c': 1}

# Nested defaultdicts
tree = defaultdict(lambda: defaultdict(list))
tree["2024"]["Jan"].append("event1")

# vs regular dict: avoids the key-check boilerplate
# d = {}
# for w in words:
#     if w[0] not in d:
#         d[w[0]] = []
#     d[w[0]].append(w)

OrderedDict & deque

deque fournit append/pop en O(1) aux deux extrémités — utilisez-le pour les files, BFS et fenêtres glissantes au lieu des listes (list.pop(0) est O(n)). Avec maxlen, deque supprime automatiquement les anciens éléments, parfait pour les tampons bornés. OrderedDict est moins nécessaire depuis 3.7 (les dicts sont ordonnés), mais move_to_end et popitem restent uniques et utiles pour les caches LRU.

python
from collections import OrderedDict, deque

# deque: double-ended queue, O(1) append/pop at both ends
dq = deque([1, 2, 3], maxlen=5)
dq.appendleft(0)   # deque([0, 1, 2, 3])
dq.append(4)       # deque([0, 1, 2, 3, 4])
dq.append(5)       # deque([1, 2, 3, 4, 5]) — oldest dropped (maxlen!)
print(dq.popleft())  # 1
print(dq)            # deque([2, 3, 4, 5])

# deque is ideal for queues, BFS, sliding windows
from collections import deque
queue = deque(["task1", "task2"])
queue.append("task3")
next_task = queue.popleft()  # FIFO — O(1) vs list.pop(0) which is O(n)

# OrderedDict: remembers insertion order (regular dicts do too in 3.7+,
# but OrderedDict has move_to_end and equality is order-sensitive)
od = OrderedDict([("a", 1), ("b", 2)])
od.move_to_end("a")  # move to last
print(list(od))  # ['b', 'a']
od.popitem(last=False)  # pop first item (FIFO)

itertools : chain, product, combinations, permutations

itertools fournit des outils rapides et efficaces en mémoire pour la combinatoire. chain aplatit les itérables paresseusement. product donne des produits cartésiens (remplace les boucles for imbriquées). combinations/permutations génèrent des sélections sans construire la liste complète — essentiel pour les entrées grandes ou infinies. Tous renvoient des itérateurs, enveloppez dans list() pour visualiser.

python
from itertools import chain, product, combinations, permutations

# chain: flatten multiple iterables
list(chain([1, 2], [3, 4], [5]))  # [1, 2, 3, 4, 5]
list(chain.from_iterable([[1, 2], [3, 4]]))  # [1, 2, 3, 4]

# product: Cartesian product (nested loops)
list(product([1, 2], ["a", "b"]))
# [(1,'a'), (1,'b'), (2,'a'), (2,'b')]
list(product("AB", repeat=2))  # [('A','A'),('A','B'),('B','A'),('B','B')]

# combinations: unordered selections (no repeats)
list(combinations("ABC", 2))  # [('A','B'),('A','C'),('B','C')]
list(combinations("AAA", 2))  # [('A','A'),('A','A'),('A','A')]

# permutations: ordered arrangements
list(permutations("ABC", 2))  # [('A','B'),('A','C'),('B','A'),('B','C'),('C','A'),('C','B')]

# combinations_with_replacement: allow picking same element
from itertools import combinations_with_replacement
list(combinations_with_replacement("AB", 2))  # [('A','A'),('A','B'),('B','B')]

itertools : groupby, accumulate, starmap

groupby regroupe les éléments consécutifs partageant une clé — triez par la clé d'abord ou vous obtiendrez plusieurs groupes pour la même clé. accumulate produit des totaux/produits cumulés. islice, takewhile et dropwhile sont des alternatives paresseuses au découpage et au filtrage qui fonctionnent sur tout itérateur, y compris infini.

python
from itertools import groupby, accumulate, starmap, islice, takewhile, dropwhile

# groupby: group consecutive items by a key (sort first!)
data = [("A", 1), ("A", 2), ("B", 3), ("B", 4), ("A", 5)]
data.sort(key=lambda x: x[0])  # MUST sort by key first
for key, group in groupby(data, key=lambda x: x[0]):
    print(key, list(group))
# A [('A',1),('A',2),('A',5)]
# B [('B',3),('B',4)]

# accumulate: running aggregate (sum by default)
list(accumulate([1, 2, 3, 4]))  # [1, 3, 6, 10]
import operator
list(accumulate([1, 2, 3, 4], operator.mul))  # [1, 2, 6, 24]

# starmap: unpack args from tuples before calling
list(starmap(pow, [(2, 3), (3, 2), (10, 3)]))  # [8, 9, 1000]

# islice: slice an iterator (doesn't support negative indices)
list(islice(range(100), 5, 10))  # [5, 6, 7, 8, 9]

# takewhile / dropwhile: filter by predicate
list(takewhile(lambda x: x < 5, [1, 4, 6, 3, 8]))  # [1, 4]
list(dropwhile(lambda x: x < 5, [1, 4, 6, 3, 8]))  # [6, 3, 8]

functools : lru_cache, partial, reduce

lru_cache mémorise les résultats — accélérations spectaculaires pour les fonctions pures récursives ou coûteuses ; cache_info() affiche les statistiques de succès/échec. partial pré-remplit les arguments pour créer des callables spécialisés. reduce applique une fonction cumulativement (bien que sum(), any(), all() le remplacent souvent). cached_property calcule une fois puis met en cache sur l'instance.

python
from functools import lru_cache, partial, reduce
import operator

# lru_cache: memoize function results (Least Recently Used)
@lru_cache(maxsize=128)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(100))  # instant (without cache: impossibly slow)
print(fib.cache_info())  # CacheInfo(hits=98, misses=101, ...)

# partial: fix some arguments, create a new callable
def power(base, exponent):
    return base ** exponent

square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5))  # 25
print(cube(3))    # 27

# reduce: cumulatively apply a function, reducing to one value
product = reduce(operator.mul, [1, 2, 3, 4])  # 24
# Equivalent: ((1*2)*3)*4

# Python 3.8+: cached_property for lazy computed attributes
from functools import cached_property
class Data:
    @cached_property
    def expensive(self):
        print("computing...")
        return [i ** 2 for i in range(1000000)]
21

Traitement JSON & CSV

json.dumps & json.loads

json.dumps() (dump string) sérialise un objet Python en chaîne JSON ; json.loads() (load string) analyse le JSON en retour. Utilisez indent pour la lisibilité, ensure_ascii=False pour garder les caractères Unicode lisibles, et sort_keys pour une sortie déterministe. Les clés JSON doivent être des chaînes — les clés int deviennent des chaînes.

python
import json

# Serialize Python object to JSON string
data = {"name": "Alice", "age": 30, "scores": [95, 88, 92]}
json_str = json.dumps(data)
print(json_str)  # {"name": "Alice", "age": 30, "scores": [95, 88, 92]}

# Pretty-print with indent
print(json.dumps(data, indent=2))
# {
#   "name": "Alice",
#   "age": 30,
#   "scores": [95, 88, 92]
# }

# Parse JSON string to Python object
parsed = json.loads(json_str)
print(parsed["name"])  # Alice
print(type(parsed["scores"]))  # <class 'list'>

# Sort keys, handle non-ASCII
print(json.dumps({"name": "Zoë"}, ensure_ascii=False, sort_keys=True))

Lire & Écrire des fichiers JSON

json.dump() écrit directement dans un objet fichier ; json.load() lit depuis un. Spécifiez toujours encoding='utf-8' pour la portabilité. Rappelez-vous du mappage de type : les objets JSON deviennent des dicts, les tableaux des listes, et les nombres des int ou float. Les datetimes, sets et objets personnalisés ne sont PAS sérialisables en JSON par défaut.

python
import json

data = {"users": [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]}

# Write to file
with open("data.json", "w", encoding="utf-8") as f:
    json.dump(data, f, indent=2, ensure_ascii=False)

# Read from file
with open("data.json", "r", encoding="utf-8") as f:
    loaded = json.load(f)

print(loaded["users"][0]["name"])  # Alice

# Type conversions to remember:
# JSON object  <-> Python dict
# JSON array   <-> Python list
# JSON string  <-> Python str
# JSON number  <-> Python int/float
# JSON true/false <-> Python True/False
# JSON null    <-> Python None

Encodage JSON personnalisé (datetime, objets personnalisés)

Le module json ne peut pas sérialiser datetime, set ou classes personnalisées par défaut. Fournissez une fonction default (appelée pour les objets non sérialisables) ou une sous-classe JSONEncoder. Pour un aller-retour, associez un encodeur personnalisé avec un object_hook dans loads() pour reconstruire les types originaux. C'est ainsi que les ORMs sérialisent les objets modèle.

python
import json
from datetime import datetime

# Default behavior: TypeError on non-serializable types
# json.dumps({"now": datetime.now()})  # TypeError!

# Solution 1: default function for unknown types
def default_encoder(obj):
    if isinstance(obj, datetime):
        return obj.isoformat()
    if isinstance(obj, set):
        return sorted(obj)
    raise TypeError(f"Cannot serialize {type(obj)}")

data = {"now": datetime.now(), "tags": {"a", "b"}}
print(json.dumps(data, default=default_encoder))

# Solution 2: custom JSONEncoder subclass
class MyEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            return {"__datetime__": obj.isoformat()}
        return super().default(obj)

print(json.dumps(data, cls=MyEncoder))

# Decoding with object_hook
def decoder(dct):
    if "__datetime__" in dct:
        return datetime.fromisoformat(dct["__datetime__"])
    return dct
json.loads(json.dumps(data, cls=MyEncoder), object_hook=decoder)

Lire des fichiers CSV

Ouvrez toujours les fichiers CSV avec newline='' pour éviter les problèmes de lignes vides sur Windows. csv.reader renvoie des listes ; csv.DictReader renvoie des dicts indexés par la ligne d'en-tête. Le module csv gère les guillemets, les virgules intégrées et les nouvelles lignes correctement — ne divisez jamais les lignes CSV manuellement avec line.split(','). Utilisez Sniffer pour détecter automatiquement les délimiteurs.

python
import csv

# Basic reader: each row is a list of strings
with open("data.csv", newline="", encoding="utf-8") as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)  # ['name', 'age', 'city']

# DictReader: each row is a dict keyed by header
with open("data.csv", newline="") as f:
    reader = csv.DictReader(f)
    for row in reader:
        print(row["name"], row["age"])  # access by column name

# Handle different delimiters and quoting
with open("data.tsv", newline="") as f:
    reader = csv.reader(f, delimiter="\t", quotechar='"')
    for row in reader:
        print(row)

# Sniffer to auto-detect format
with open("unknown.csv", newline="") as f:
    sample = f.read(1024)
    dialect = csv.Sniffer().sniff(sample)
    f.seek(0)
    reader = csv.reader(f, dialect)

Écrire des fichiers CSV

csv.writer écrit des listes ; csv.DictWriter écrit des dicts avec un ensemble fixe de noms de champs. Utilisez toujours newline='' lors de l'ouverture du fichier. Le paramètre quoting contrôle quand les champs sont entre guillemets — QUOTE_MINIMAL (par défaut) ne met entre guillemets que lorsque nécessaire, QUOTE_ALL met tout entre guillemets, utile pour les analyseurs stricts.

python
import csv

rows = [
    ["name", "age", "city"],
    ["Alice", 30, "NYC"],
    ["Bob", 25, "LA"],
]

# Basic writer
with open("out.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerows(rows)  # write multiple rows

# DictWriter: write from dicts
with open("out.csv", "w", newline="") as f:
    fieldnames = ["name", "age", "city"]
    writer = csv.DictWriter(f, fieldnames=fieldnames)
    writer.writeheader()
    writer.writerow({"name": "Alice", "age": 30, "city": "NYC"})
    writer.writerow({"name": "Bob", "age": 25, "city": "LA"})

# Control quoting: QUOTE_MINIMAL (default), QUOTE_ALL, QUOTE_NONNUMERIC
writer = csv.writer(f, quoting=csv.QUOTE_ALL)
# QUOTE_ALL wraps every field in quotes: "Alice","30","NYC"

JSON Lines (NDJSON) & Streaming

JSON Lines (NDJSON) met un objet JSON par ligne — idéal pour les logs, les flux d'événements et les fichiers à ajout uniquement car vous pouvez traiter chaque ligne indépendamment. Pour les énormes documents JSON uniques, utilisez la bibliothèque ijson pour analyser en flux sans charger tout le fichier en mémoire. NDJSON est la norme pour de nombreux pipelines de données.

python
import json

# JSON Lines: one JSON object per line (great for logs, big data)
records = [{"id": 1, "msg": "first"}, {"id": 2, "msg": "second"}]

# Write NDJSON
with open("logs.jsonl", "w") as f:
    for rec in records:
        f.write(json.dumps(rec) + "\n")

# Read NDJSON line by line (memory-efficient for huge files)
with open("logs.jsonl", "r") as f:
    for line in f:
        rec = json.loads(line)
        print(rec["id"], rec["msg"])

# Stream large JSON arrays without loading everything into memory
# Use ijson library for streaming parsing of huge JSON files:
# import ijson
# with open("huge.json", "rb") as f:
#     for item in ijson.items(f, "items.item"):
#         process(item)  # one item at a time
22

Journalisation & Tests

Bases de logging

Le module logging est la méthode standard pour émettre une sortie de diagnostic — bien meilleur que print() car vous contrôlez les niveaux, formats et destinations. Utilisez logging.getLogger(__name__) par module pour pouvoir régler la verbosité par module. logging.exception() inclut automatiquement la trace. Configurez basicConfig une fois au démarrage.

python
import logging

# Basic configuration (call once at program start)
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

# Log levels (severity ascending)
logging.debug("Detailed debug info")     # DEBUG (10)
logging.info("General information")       # INFO (20)
logging.warning("Something unexpected")   # WARNING (30)
logging.error("A real error occurred")    # ERROR (40)
logging.critical("System is down")        # CRITICAL (50)

# Logging exceptions with traceback
try:
    1 / 0
except ZeroDivisionError:
    logging.exception("Division failed")  # includes full traceback

# Get a named logger (best practice per module)
logger = logging.getLogger(__name__)
logger.info("Module-specific log")

Journalisation vers fichier & Handlers multiples

Les Handlers routent les enregistrements de log vers des destinations — console, fichiers, réseau, email. RotatingFileHandler plafonne la taille du fichier et conserve des sauvegardes, empêchant la croissance illimitée des logs. Chaque handler peut avoir son propre niveau et format (ex. logs détaillés vers fichier, logs concis vers console). TimedRotatingFileHandler pivote par temps plutôt que par taille.

python
import logging
from logging.handlers import RotatingFileHandler

logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)

# Console handler (INFO and above)
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))

# Rotating file handler (DEBUG and above, max 5MB x 3 backups)
file_handler = RotatingFileHandler(
    "app.log", maxBytes=5_000_000, backupCount=3, encoding="utf-8"
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(
    logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")
)

logger.addHandler(console)
logger.addHandler(file_handler)

logger.debug("debug to file only")
logger.info("info to both console and file")
logger.error("error everywhere")

Bases d'unittest

unittest est le framework de test intégré de Python (style xUnit). Les tests vivent dans des classes héritant de TestCase. setUp/tearDown s'exécutent avant/après chaque test pour l'isolation. Assertions courantes : assertEqual, assertTrue, assertRaises, assertIn. Exécutez avec python -m unittest pour la découverte automatique des fichiers test_*.py.

python
import unittest

def add(a, b):
    return a + b

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

class TestMath(unittest.TestCase):
    def setUp(self):
        # runs before each test method
        self.data = [1, 2, 3]

    def tearDown(self):
        # runs after each test method
        pass

    def test_add(self):
        self.assertEqual(add(1, 2), 3)
        self.assertEqual(add(-1, 1), 0)

    def test_add_types(self):
        self.assertEqual(add("a", "b"), "ab")

    def test_divide_by_zero(self):
        with self.assertRaises(ValueError):
            divide(1, 0)

    def test_membership(self):
        self.assertIn(2, self.data)
        self.assertTrue(3 in self.data)

if __name__ == "__main__":
    unittest.main()
# Run: python -m unittest test_file.py -v

Bases de pytest

pytest est l'outil de test Python le plus populaire — de simples instructions assert donnent des rapports d'échec riches, sans classes passe-partout. pytest.raises vérifie les exceptions avec correspondance regex optionnelle. pytest.approx gère l'imprécision de comparaison des flottants. Installez avec pip install pytest et exécutez avec pytest -v pour une sortie détaillée.

python
# test_math.py — pytest is simpler and more powerful than unittest
# Install: pip install pytest
# Run: pytest -v

def add(a, b):
    return a + b

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

# Plain functions, no classes required
def test_add():
    assert add(1, 2) == 3
    assert add(-1, 1) == 0

def test_add_strings():
    assert add("hello", " world") == "hello world"

# Testing exceptions with pytest.raises
import pytest

def test_divide_by_zero():
    with pytest.raises(ValueError, match="Cannot divide by zero"):
        divide(1, 0)

# Approximate float comparison
def test_float():
    assert 0.1 + 0.2 == pytest.approx(0.3)

Fixtures pytest

Les fixtures sont l'injection de dépendances de pytest — elles fournissent des données de configuration, des objets mock ou des ressources aux tests via des noms de paramètres. Les fixtures basées sur yield gèrent à la fois la configuration (avant yield) et le démontage (après yield). Les portées contrôlent la réutilisation : 'session' crée la fixture une fois pour toute l'exécution, 'module' une fois par fichier, 'function' (par défaut) une fois par test.

python
import pytest

# A fixture provides setup data/resources to tests
@pytest.fixture
def sample_list():
    return [1, 2, 3, 4, 5]

# Use fixtures by passing their name as a parameter
def test_length(sample_list):
    assert len(sample_list) == 5

def test_sum(sample_list):
    assert sum(sample_list) == 15

# Fixture with setup AND teardown (yield)
@pytest.fixture
def db_connection():
    print("\n[setup] connecting to DB")
    conn = {"connected": True}
    yield conn  # test runs here; value passed to test
    print("\n[teardown] closing DB")
    conn["connected"] = False

def test_db(db_connection):
    assert db_connection["connected"] is True

# Fixture scopes: function (default), class, module, session
@pytest.fixture(scope="session")
def expensive_resource():
    return load_large_dataset()  # created once per test session

parametrize & mocking dans pytest

parametrize exécute une seule fonction de test sur plusieurs jeux d'entrées — élimine le copier-coller de code de test et donne une sortie claire par cas. unittest.mock.patch remplace des fonctions/objets par des mocks pour des tests isolés. assert_called_once_with vérifie que le mock a été utilisé correctement. @pytest.mark.skip et xfail gèrent les tests incomplets gracieusement.

python
import pytest
from unittest.mock import patch, MagicMock

# parametrize: run one test with multiple inputs
@pytest.mark.parametrize("a, b, expected", [
    (1, 2, 3),
    (-1, 1, 0),
    (0, 0, 0),
    (100, 200, 300),
])
def test_add_many(a, b, expected):
    assert add(a, b) == expected

# parametrize with IDs for readable output
@pytest.mark.parametrize("x", [1, 2, 3], ids=["one", "two", "three"])
def test_ids(x):
    assert x > 0

# Mocking: replace external dependencies
def fetch_user(uid):
    # imagine this calls a real API
    return {"id": uid, "name": "real_user"}

@patch("__main__.fetch_user")
def test_with_mock(mock_fetch):
    mock_fetch.return_value = {"id": 1, "name": "mocked"}
    result = fetch_user(1)
    assert result["name"] == "mocked"
    mock_fetch.assert_called_once_with(1)

# Skip and expected failure
@pytest.mark.skip(reason="not implemented yet")
def test_future():
    pass

@pytest.mark.xfail(reason="known bug #42")
def test_known_bug():
    assert 1 == 2
23

Programmation réseau

Serveur TCP

Crée un serveur TCP en utilisant le module socket. bind associe le socket à une adresse, listen définit la file d'attente, accept bloque jusqu'à ce qu'un client se connecte. Fermez toujours les connexions pour libérer les descripteurs de fichiers.

python
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('localhost', 8080))
server.listen(5)
conn, addr = server.accept()
data = conn.recv(1024)
conn.sendall(b'Hello')
conn.close()

Client TCP

Crée un client TCP qui se connecte à un serveur. connect établit la connexion, sendall envoie tous les octets, recv lit jusqu'au nombre d'octets spécifié. Utilisez encode/decode pour la conversion chaîne-vers-octets.

python
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('localhost', 8080))
client.sendall(b'Hello Server')
response = client.recv(1024)
print(response.decode())
client.close()

Socket UDP

UDP est sans connexion : pas de handshake, pas de livraison garantie. recvfrom renvoie à la fois les données et l'adresse de l'expéditeur. Utilisez SOCK_DGRAM pour UDP. Idéal pour DNS, les jeux et le streaming en temps réel.

python
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('localhost', 9090))
data, addr = sock.recvfrom(1024)
print(f"From {addr}: {data.decode()}")
sock.sendto(b'Reply', addr)

Serveur HTTP

Le module http.server fournit un serveur HTTP simple. Sous-classez BaseHTTPRequestHandler et redéfinissez do_GET, do_POST. À utiliser pour le développement uniquement ; utilisez gunicorn pour la production.

python
from http.server import HTTPServer, BaseHTTPRequestHandler
class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-Type', 'text/html')
        self.end_headers()
        self.wfile.write(b'<h1>Hello</h1>')
HTTPServer(('localhost', 8000), Handler).serve_forever()

Délai d'attente du socket

settimeout définit un délai d'attente pour toutes les opérations de socket. Si une opération dépasse le délai, une exception socket.timeout est levée. Utilisez try/finally pour garantir le nettoyage.

python
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5.0)
try:
    sock.connect(('example.com', 80))
    data = sock.recv(1024)
except socket.timeout:
    print('Connection timed out')
finally:
    sock.close()
24

Base de données (SQLite)

Créer une table

sqlite3 est intégré à Python. connect crée ou ouvre un fichier de base de données. CREATE TABLE IF NOT EXISTS empêche les erreurs si la table existe. Appelez toujours commit pour enregistrer les modifications.

python
import sqlite3
conn = sqlite3.connect('example.db')
cursor = conn.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL, email TEXT UNIQUE, age INTEGER)''')
conn.commit()

Insérer des données

Utilisez toujours des requêtes paramétrées (placeholders ?) pour empêcher l'injection SQL. lastrowid renvoie l'ID auto-incrémenté. N'utilisez jamais le formatage de chaîne pour les valeurs SQL.

python
cursor.execute(
    'INSERT INTO users (name, email, age) VALUES (?, ?, ?)',
    ('Alice', '[email protected]', 30))
conn.commit()
print(f"ID: {cursor.lastrowid}")

Interroger des données

fetchall renvoie toutes les lignes correspondantes sous forme de liste de tuples. fetchone renvoie une seule ligne ou None. Pour les grands jeux de résultats, itérez directement sur le curseur.

python
cursor.execute('SELECT * FROM users WHERE age > ?', (25,))
rows = cursor.fetchall()
for row in rows:
    print(row)
cursor.execute('SELECT * FROM users WHERE id = ?', (1,))
user = cursor.fetchone()

Mettre à jour & Supprimer

UPDATE modifie les lignes existantes, DELETE les supprime. rowcount indique les lignes affectées. Utilisez toujours WHERE avec DELETE. commit persiste les modifications.

python
cursor.execute('UPDATE users SET age = ? WHERE name = ?', (31, 'Alice'))
cursor.execute('DELETE FROM users WHERE age < ?', (18,))
conn.commit()
print(f"Affected: {cursor.rowcount} rows")

Fabrique de lignes

Utiliser conn comme gestionnaire de contexte valide automatiquement en cas de succès et annule en cas d'exception. row_factory = sqlite3.Row permet d'accéder aux colonnes par nom.

python
conn = sqlite3.connect('example.db')
conn.row_factory = sqlite3.Row
with conn:
    conn.execute('INSERT INTO users (name, email) VALUES (?, ?)',
                 ('Bob', '[email protected]'))
for row in conn.execute('SELECT * FROM users'):
    print(row['name'], row['email'])
25

Web Scraping

Bases de BeautifulSoup

requests récupère le contenu HTML, BeautifulSoup l'analyse. html.parser est intégré ; lxml est plus rapide. Vérifiez toujours response.status_code avant l'analyse.

python
import requests
from bs4 import BeautifulSoup
resp = requests.get('https://example.com')
soup = BeautifulSoup(resp.text, 'html.parser')
print(soup.title.string)
print(soup.find('h1').text)

Trouver des éléments

find_all renvoie tous les éléments correspondants, find renvoie le premier. Utilisez class_ (avec tiret bas). select utilise les sélecteurs CSS pour les requêtes complexes.

python
links = soup.find_all('a')
for link in links:
    print(link.get('href'), link.text)
article = soup.find('div', class_='article')
items = soup.select('ul.list > li.item')

Extraire des tables

Les tables sont structurées en tr (lignes) contenant des cellules td (données) ou th (en-tête). strip supprime les espaces. find_all accepte une liste de noms de balises.

python
table = soup.find('table')
for row in table.find_all('tr'):
    cols = row.find_all(['td', 'th'])
    data = [col.text.strip() for col in cols]
    print(data)

Gérer la pagination

La pagination est gérée en suivant les liens de page suivante. select_one renvoie la première correspondance ou None. Ajoutez time.sleep entre les requêtes.

python
all_items = []
url = 'https://example.com/page/1'
while url:
    resp = requests.get(url)
    soup = BeautifulSoup(resp.text, 'html.parser')
    all_items.extend([i.text for i in soup.select('.item')])
    next_link = soup.select_one('a.next')
    url = next_link.get('href') if next_link else None

Enregistrer en CSV

csv.DictWriter écrit des dictionnaires en CSV. newline empêche les lignes vides supplémentaires sur Windows. encoding=utf-8 gère les caractères spéciaux.

python
import csv
with open('data.csv', 'w', newline='', encoding='utf-8') as f:
    writer = csv.DictWriter(f, fieldnames=['name', 'price'])
    writer.writeheader()
    for item in scraped_data:
        writer.writerow(item)
26

Web async (aiohttp)

Client HTTP

aiohttp fournit du HTTP async. ClientSession gère le pool de connexions. async with garantit le nettoyage. asyncio.run exécute la coroutine.

python
import aiohttp, asyncio
async def fetch(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            return await resp.text()
data = asyncio.run(fetch('https://api.example.com'))

Requêtes concurrentes

asyncio.gather exécute des coroutines de manière concurrente, réduisant le temps total. Toutes les requêtes partagent la même session. Utilisez un sémaphore pour limiter la concurrence.

python
async def fetch_all(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [session.get(url) for url in urls]
        responses = await asyncio.gather(*tasks)
        return [await r.text() for r in responses]

Serveur Web

aiohttp.web crée des serveurs web async. Les routes sont définies avec la méthode HTTP et le motif de chemin. match_info extrait les paramètres de chemin.

python
from aiohttp import web
async def handle(request):
    name = request.match_info.get('name', 'World')
    return web.json_response({'message': f'Hello, {name}!'})
app = web.Application()
app.add_routes([web.get('/', handle), web.get('/{name}', handle)])
web.run_app(app, port=8080)

Serveur WebSocket

Les WebSockets permettent une communication en temps réel bidirectionnelle. WebSocketResponse gère le handshake de mise à niveau. async for itère sur les messages.

python
async def ws_handler(request):
    ws = web.WebSocketResponse()
    await ws.prepare(request)
    async for msg in ws:
        if msg.type == aiohttp.WSMsgType.TEXT:
            await ws.send_str(f'Echo: {msg.data}')
    return ws

Session avec cookies

ClientSession persiste les cookies entre les requêtes automatiquement. Essentiel pour le scraping authentifié. Utilisez une seule session pour toutes les requêtes.

python
async def login_and_fetch():
    async with aiohttp.ClientSession() as session:
        await session.post('https://example.com/login',
                          data={'user': 'admin', 'pass': '123'})
        resp = await session.get('https://example.com/dashboard')
        return await resp.text()
27

Multiprocessing approfondi

Pool de processus

Pool gère les processus workers. map distribue le travail en parallèle. apply_async exécute une seule fonction de manière asynchrone. Utilisez toujours la garde if __name__ == main sur Windows.

python
from multiprocessing import Pool
def square(x): return x * x
if __name__ == '__main__':
    with Pool(4) as pool:
        results = pool.map(square, range(10))
        result = pool.apply_async(square, (100,))
        print(result.get(timeout=5))

Mémoire partagée

Value et Array créent de la mémoire partagée entre processus. Utilisez get_lock pour synchroniser l'accès et empêcher les conditions de course.

python
from multiprocessing import Value, Array
counter = Value('i', 0)
arr = Array('d', [0.0, 1.0, 2.0])
with counter.get_lock():
    counter.value += 1

Communication par Queue

Queue permet une communication sûre entre processus. put ajoute des éléments, get les récupère. Queue est sécurisé pour les processus, gérant le verrouillage en interne.

python
from multiprocessing import Process, Queue
def worker(q):
    q.put('Data from worker')
if __name__ == '__main__':
    q = Queue()
    p = Process(target=worker, args=(q,))
    p.start()
    print(q.get())
    p.join()

Pipe

Pipe crée un canal de communication bidirectionnel. send et recv transmettent des objets Python via le pickling. Pipe est plus rapide que Queue pour la communication point à point.

python
from multiprocessing import Process, Pipe
def worker(conn):
    conn.send(['hello', 'world'])
    msg = conn.recv()
    conn.close()
if __name__ == '__main__':
    parent, child = Pipe()
    p = Process(target=worker, args=(child,))
    p.start()
    print(parent.recv())
    parent.send('acknowledged')
    p.join()

Synchronisation

Lock garantit qu'un seul processus accède à une ressource partagée à la fois. with lock acquiert et libère automatiquement. Autres primitives : RLock, Semaphore, Event.

python
from multiprocessing import Process, Lock
def safe_print(lock, msg):
    with lock:
        print(msg)
if __name__ == '__main__':
    lock = Lock()
    procs = [Process(target=safe_print, args=(lock, f'Task {i}'))
             for i in range(5)]
    for p in procs: p.start()
    for p in procs: p.join()
28

Environnements virtuels approfondis

Module venv

venv crée des environnements Python isolés avec leurs propres répertoires de packages. L'activation modifie PATH. Activez toujours avant d'installer des dépendances.

python
# Create
python -m venv myenv
# Activate (Linux/Mac)
source myenv/bin/activate
# Activate (Windows)
myenv\Scripts\activate
# Deactivate
deactivate

requirements.txt

requirements.txt liste les dépendances du projet. == épingle les versions exactes, >= autorise les mises à niveau dans une plage. Committez toujours au contrôle de version.

python
# Generate
pip freeze > requirements.txt
# Install
pip install -r requirements.txt
# Pin versions
flask==2.3.3
requests>=2.28.0,<3.0.0

Poetry

Poetry est un gestionnaire de dépendances moderne. pyproject.toml remplace requirements.txt. Les environnements virtuels sont gérés automatiquement.

python
# Initialize
poetry init
# Add dependency
poetry add flask
poetry add pytest --group dev
# Install all
poetry install
# Run command
poetry run python app.py

pipenv

pipenv combine pip et virtualenv. Pipfile déclare les dépendances, Pipfile.lock épingle les versions exactes. --dev sépare les dépendances de développement.

python
# Create environment
pipenv install
# Add package
pipenv install requests
pipenv install pytest --dev
# Activate shell
pipenv shell
# Run command
pipenv run python app.py

Environnements Conda

Conda gère à la fois les dépendances Python et non-Python. environment.yml capture l'environnement complet. Idéal pour la data science avec des dépendances binaires.

python
# Create
conda create -n myenv python=3.11
# Activate
conda activate myenv
# Export
conda env export > environment.yml
# Recreate
conda env create -f environment.yml
29

Utilisation avancée de pip

Installer depuis Git

Installez des packages directement depuis des dépôts Git. Utile pour les versions non publiées, les forks ou les packages privés. @branch ou @commit épingle à une version spécifique.

python
# From GitHub
pip install git+https://github.com/user/repo.git
# Specific branch
pip install git+https://github.com/user/repo.git@branch-name
# Specific commit
pip install git+https://github.com/user/repo.git@abc123

Installation éditable

L'installation éditable (-e) lie le package au lieu de le copier. Les modifications sont immédiatement disponibles sans réinstallation. Essentiel pour le développement de packages.

python
# Install in development mode
pip install -e .
# From a specific path
pip install -e /path/to/package
# With extras
pip install -e ".[dev,test]"

Contraintes & Hachages

Les contraintes limitent les versions installables. La vérification de hachage vérifie l'intégrité du package, empêchant les attaques de chaîne d'approvisionnement.

python
# constraints.txt
flask==2.3.3
pip install -c constraints.txt flask
# Hash checking
pip install --require-hashes -r requirements.txt

Gestion du cache

pip met en cache les wheels téléchargées. --no-cache-dir force les téléchargements frais. Purge libère de l'espace disque lorsque le cache devient trop volumineux.

python
# Show cache info
pip cache info
# List cached packages
pip cache list
# Purge entire cache
pip cache purge
# Install with no cache
pip install --no-cache-dir flask

Index personnalisé

--index-url spécifie un dépôt de packages personnalisé. --extra-index-url ajoute un secours. --trusted-host contourne SSL pour les registres internes.

python
# Use custom index
pip install --index-url https://pypi.custom.com/simple/ flask
# Extra index (fallback)
pip install --extra-index-url https://pypi.custom.com/simple/ flask
# Trusted host (no SSL)
pip install --trusted-host pypi.custom.com flask
30

Vérification de type (mypy)

Indications de type de base

Les indications de type annotent les paramètres de fonction et les types de retour. Python 3.9+ autorise les types intégrés directement. Les indications activent l'analyse statique avec mypy.

python
def greet(name: str, times: int = 1) -> str:
    return (f"Hello, {name}! " * times).strip()
def process(data: list[int]) -> dict[str, int]:
    return {str(x): x for x in data}

Optional et Union

Optional[X] est équivalent à X | None (Python 3.10+). Les types Union autorisent plusieurs types possibles. mypy vérifie que tous les chemins de code gèrent tous les types.

python
from typing import Optional
def find(items: list[int], target: int) -> int | None:
    for i, v in enumerate(items):
        if v == target: return i
    return None

Types génériques

Les génériques créent des conteneurs réutilisables et de type sûr. TypeVar définit une variable de type, Generic rend la classe générique. mypy assure la cohérence des types.

python
from typing import TypeVar, Generic
T = TypeVar('T')
class Stack(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []
    def push(self, item: T) -> None:
        self._items.append(item)
    def pop(self) -> T:
        return self._items.pop()

Protocol

Protocol définit le sous-typage structurel (typage canard avec vérification de type). Toute classe avec les méthodes requises satisfait le protocole, aucune héritage nécessaire.

python
from typing import Protocol
class Closeable(Protocol):
    def close(self) -> None: ...
def cleanup(resource: Closeable) -> None:
    resource.close()
class File:
    def close(self) -> None: print("Closed")
cleanup(File())  # OK - File has close()

Configuration mypy

mypy.ini configure la rigueur de la vérification de type. strict active toutes les vérifications. Les remplacements par module assouplissent les règles pour les tests ou le code hérité.

python
# mypy.ini
[mypy]
python_version = 3.11
strict = True
warn_return_any = True
disallow_untyped_defs = True
[mypy-tests.*]
ignore_errors = True
31

Conseils de performance

Liste vs Générateur

Les listes stockent tous les éléments en mémoire ; les générateurs produisent des valeurs à la demande. Utilisez les générateurs pour les grandes séquences itérées une fois.

python
# List: all in memory
squares = [x**2 for x in range(1000000)]
# Generator: lazy evaluation
squares_gen = (x**2 for x in range(1000000))
import sys
print(sys.getsizeof(squares))      # ~8MB
print(sys.getsizeof(squares_gen))  # ~200 bytes

Concaténation de chaînes

La concaténation de chaînes avec += est O(n^2). join est O(n). Les f-strings sont la méthode d'interpolation la plus rapide.

python
# Slow: creates intermediate strings
result = ""
for s in parts:
    result += s
# Fast: join in one operation
result = "".join(parts)
# Fast: f-strings
msg = f"Hello, {name}!"

Variables locales

Les recherches de variables locales sont plus rapides que les recherches globales ou d'attribut. Affecter les fonctions fréquemment utilisées à des locales accélère les boucles.

python
import math
# Slow: global lookup
def compute_slow(values):
    return [math.sqrt(v) for v in values]
# Fast: local reference
def compute_fast(values):
    sqrt = math.sqrt
    return [sqrt(v) for v in values]

__slots__

__slots__ empêche la création de __dict__, économisant 40-50% de mémoire par instance. Significatif lors de la création de millions d'objets. Impossible d'ajouter des attributs non listés.

python
class Point:
    __slots__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y
p = Point(1, 2)
# p.z = 3  # AttributeError

timeit & cProfile

timeit mesure le temps d'exécution de petits extraits. cProfile montre où le temps est passé. Utilisez le profilage avant d'optimiser pour trouver les vrais goulots d'étranglement.

python
import timeit
t = timeit.timeit('sum(range(100))', number=10000)
print(f"{t:.4f}s")
import cProfile
cProfile.run('sum(x**2 for x in range(10000))')
32

Pièges courants

Arguments par défaut mutables

Les valeurs d'arguments par défaut sont évaluées une fois au moment de la définition. Les valeurs par défaut mutables sont partagées entre tous les appels. Utilisez toujours None comme valeur par défaut.

python
# BUG: default list is shared
def add_item(item, lst=[]):
    lst.append(item)
    return lst
print(add_item(1))  # [1]
print(add_item(2))  # [1, 2]!
# FIX: use None
def add_item(item, lst=None):
    if lst is None: lst = []
    lst.append(item)
    return lst

Liaison tardive des fermetures

Les fermetures capturent les variables par référence. Au moment où les lambdas sont appelés, la variable de boucle a sa valeur finale. Les arguments par défaut capturent la valeur courante.

python
# BUG: all print 2
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])  # [2, 2, 2]
# FIX: default argument
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])  # [0, 1, 2]

Mise en cache des entiers

Python met en cache les petits entiers. is vérifie l'identité, == vérifie l'égalité. N'utilisez jamais is pour la comparaison de valeurs ; utilisez is uniquement pour None, True, False.

python
# Small integers cached (-5 to 256)
a = 256; b = 256
print(a is b)  # True (cached)
c = 257; d = 257
print(c is d)  # False (not cached)
print(c == d)  # True

is vs ==

is vérifie si deux références pointent vers le même objet. == vérifie si deux objets ont la même valeur. Utilisez is uniquement pour None, True, False.

python
a = [1, 2, 3]; b = [1, 2, 3]
print(a == b)  # True (same values)
print(a is b)  # False (different objects)
# Correct usage of is
if x is None: ...
if x is not None: ...

GIL

Le GIL permet à un seul thread d'exécuter le bytecode Python à la fois. Le threading est efficace pour les tâches liées aux E/S. Utilisez le multiprocessing pour le parallélisme lié au CPU.

python
# GIL prevents true parallelism for CPU-bound tasks
import threading
def cpu_work():
    total = sum(i**2 for i in range(10**7))
# Use multiprocessing for CPU work
from multiprocessing import Pool
with Pool(4) as p:
    p.map(cpu_work, range(4))

Snippets Python associés

Copy-paste ready code for common tasks.

Trier un dictionnaire par valeur

Trier un dictionnaire Python par ses valeurs en ordre décroissant.

List comprehension

Générer rapidement des listes en utilisant des list comprehensions.

Fusion de dictionnaires

Plusieurs façons de fusionner des dictionnaires.

Lecture/écriture de fichiers

Diverses façons de lire et écrire des fichiers.

Traitement CSV

Lire et écrire des fichiers CSV avec le module csv.

Traitement JSON

Sérialisation et désérialisation JSON.

Correspondance regex

Effectuer une correspondance regex en utilisant le module re.

Gestion des dates

Gérer les dates et heures avec datetime.

Décorateurs

Définir et utiliser des décorateurs.

Générateurs

Économiser de la mémoire en utilisant des générateurs.

Gestionnaire de contexte

Gestionnaires de contexte personnalisés.

Gestion des exceptions

Mécanisme complet de gestion des exceptions.

Héritage de classe

Héritage de classe et surcharge de méthodes.

Multithreading

Implémenter le multithreading avec le module threading.

Multiprocessing

Atteindre un véritable parallélisme avec multiprocessing.

Programmation asynchrone asyncio

Implémenter la concurrence asynchrone avec asyncio.

Programmation Socket

Serveur et client TCP Socket.

Requêtes HTTP

Envoyer des requêtes HTTP avec la bibliothèque requests.

Opérations sur base de données

Opérer sur des bases de données avec sqlite3.

Environnement virtuel

Créer et gérer des environnements virtuels Python.

Installation pip

Commandes courantes de gestion de packages pip.

Variables d'environnement

Lire et définir des variables d'environnement.

Journalisation

Configurer et utiliser le module logging.

Tests unitaires

Écrire des tests unitaires avec unittest.

Indications de type

Améliorer la lisibilité du code avec des annotations de type.

Dataclass

Simplifier les définitions de classe avec dataclass.

Enum

Définir des types enum avec Enum.

Décorateur property

Contrôler l'accès aux attributs avec property.

Méthodes magiques

Exemples courants de méthodes magiques.

Itérateur

Implémentation d'itérateur personnalisé.

Coroutine

Utilisation de base des coroutines.

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.