Cas d'usages concrets des dataclasses Python
Article publié le 26/04/2025 par Jules SAGOT
Parser une réponse API avec une dataclass
On commence par définir notre schéma de données qu’on va obtenir via API avec le module dataclass :
from dataclasses import dataclass
@dataclass
class Address:
street: str
city: str
zipcode: str
@dataclass
class User:
id: int
name: str
email: str
address: Address
Une réponse API inclue souvent plus de champs que ce dont on a besoin.
Le problème étant qu’il n’est pas possible de passer plus d’arguments que prévu à une dataclass :
Address(street='Kulas Light', city='Gwenborough', zipcode='92998-3874', unexpected_field=42)
⬇️💣
TypeError: Address.__init__() got an unexpected keyword argument 'unexpected_field'
Afin d’éviter cette erreur, on crée un utilitaire parse_api
qui
permet de passer uniquement les champs déclarés aux dataclasses :
from dataclasses import dataclass, fields
from typing import Any, Dict
def parse_api(cls, data: Dict[str, Any]):
init_kwargs = {}
for field in fields(cls):
field_name = field.name
field_type = field.type
if field_name in data:
value = data[field_name]
# If the field is a dataclass itself
if hasattr(field_type, '__dataclass_fields__'):
value = parse_api(field_type, value)
init_kwargs[field_name] = value
return cls(**init_kwargs)
Ce qui donne sur un exemple complet :
api_response = {
"id": 1,
"name": "Leanne Graham",
"email": "Sincere@april.biz",
"address": {
"id": 1,
"street": "Kulas Light",
"city": "Gwenborough",
"zipcode": "92998-3874"
},
"extra_field": "is ignored"
}
print(parse_api(User, api_response))
⬇️
User(id=1, name='Leanne Graham', email='Sincere@april.biz', address=Address(street='Kulas Light', city='Gwenborough', zipcode='92998-3874'))
Le module dataclass permet, sans dépendance externe, de rapidement créer des objects qui vont contenir la donnée qui vient de notre API.
Voici les avantages par rapport à un dictionnaire :
- On a défini les champs retournés par l’API dans notre dataclass
- Notre éditeur de texte va permettre d’auto-compléter les champs disponibles dans la suite du code
- Solution performante car sans typage fort et basée sur la librairie standard Python
Cette solution est idéale si votre projet dépends peu d’API externes.
Si vous avez besoin d’un typage fort et garanti sur les champs, vous pouvez utiliser un module tiers comme pandera.
Configurer son application avec des dataclasses
Les dataclasses peuvent être utilisées pour sauvegarder la configuration de l’application web à son démarrage.
On va rendre la configuration non modifiable avec frozen=True
.
La configuration sera stockée en YAML dans config.yaml
. Python
dispose aussi d’un module natif Python qui permet de lire le YAML.
debug: true
secret_key: iowkdsvomkfegrjfmdslqkplezfkogejtb
database:
host: localhost
port: 5432
username: postgres
password: postgres
database_name: postgres
On déclare des dataclasses qui vont contenir notre configuration en lecture seule.
Ensuite, on déclare un utilitaire pour transformer le fichier de configuration en instances de dataclass.
Enfin, on utilise la configuration obtenue.
from dataclasses import dataclass
import yaml
@dataclass(frozen=True)
class DatabaseConfig:
host: str
port: int
username: str
password: str
database_name: str
@dataclass(frozen=True)
class AppConfig:
debug: bool
secret_key: str
database: DatabaseConfig
def load_config_from_yaml(path: str) -> AppConfig:
with open(path, 'r') as file:
data = yaml.safe_load(file)
database_data = data['database']
return AppConfig(
debug=data['debug'],
secret_key=data['secret_key'],
database=DatabaseConfig(
host=database_data['host'],
port=database_data['port'],
username=database_data['username'],
password=database_data['password'],
database_name=database_data['database_name']
)
)
config = load_config_from_yaml("config.yaml")
print(f"Debug Mode: {config.debug}")
print(f"Database Host: {config.database.host}")
⬇️
Debug Mode: True
Database Host: localhost
Gérer les dépendances de tâches avec un DAG et des dataclass
On peut aussi utiliser les dataclasses dans des algorithmes, pour
éviter d’avoir à écrire la fonction __init__
.
Pour l’exemple, on va créer un ensemble de tâches qui vont dépendre les unes des autres.
Une tâche a des dépendances et doit exécuter un code pour être terminée.
Ensuite, on crée un graphe orienté acyclique (DAG en anglais) qui va permettre d’éxecuter les tâches de manière synchrone, et dans le bon ordre en fonction de leurs dépendances.
from dataclasses import dataclass, field
from typing import List, Dict, Callable, Set
@dataclass
class Node:
name: str
func: Callable
dependencies: List[str] = field(default_factory=list)
def run(self, context: Dict[str, any]) -> any:
# Run the function with inputs from context
inputs = [context[dep] for dep in self.dependencies]
output = self.func(*inputs)
print(f"Node {self.name}: {output}")
return output
@dataclass
class DAG:
nodes: Dict[str, Node] = field(default_factory=dict)
def add_node(self, node: Node):
if node.name in self.nodes:
raise ValueError(f"Node {node.name} already exists in DAG.")
self.nodes[node.name] = node
def _topological_sort(self) -> List[str]:
visited = set()
temp_marks = set()
result = []
def visit(node_name: str):
if node_name in temp_marks:
raise Exception(f"Cycle detected at {node_name}")
if node_name not in visited:
temp_marks.add(node_name)
for dep in self.nodes[node_name].dependencies:
visit(dep)
temp_marks.remove(node_name)
visited.add(node_name)
result.append(node_name)
for node_name in self.nodes:
visit(node_name)
return result
def execute(self) -> Dict[str, any]:
order = self._topological_sort()
context = {}
for node_name in order:
node = self.nodes[node_name]
context[node_name] = node.run(context)
return context
# Example usage:
# Define some simple functions
def start():
return "start"
def step1(x):
return f"{x} -> step1"
def step2(x):
return f"{x} -> step2"
def final(x, y):
return f"final({x}, {y})"
# Build the DAG
dag = DAG()
dag.add_node(Node(name="start", func=start))
dag.add_node(Node(name="step1", func=step1, dependencies=["start"]))
dag.add_node(Node(name="step2", func=step2, dependencies=["start"]))
dag.add_node(Node(name="final", func=final, dependencies=["step1", "step2"]))
# Execute the DAG
dag.execute()
⬇️
Node start: start
Node step1: start -> step1
Node step2: start -> step2
Node final: final(start -> step1, start -> step2)