🐍 Module Python — Refresh rapide pour Odoo

Les 11 concepts Python qu'il faut maîtriser AVANT de toucher au code Odoo. Refresh ciblé, pas un cours complet.

2-3h Pré-requis Module 0
🎯
À qui s'adresse ce module ?

Vous connaissez déjà Python (Django, Flask, scripts, data). Ce module est un refresh des concepts massivement utilisés en Odoo, pour ne pas être bloqué sur la syntaxe au moment d'aborder l'ORM. Si vous êtes vraiment débutant Python, faites d'abord un MOOC type "Python for Everybody" avant.

🎯 Au programme

  1. Python 3.10+ (les nouveautés que Odoo 19 utilise)
  2. Classes & héritage (multi-inheritance, MRO, super())
  3. Décorateurs (le coeur de l'API Odoo)
  4. Compréhensions (list / dict / set)
  5. Lambda + filter / map (utilisés sur tous les recordsets)
  6. Gestion des exceptions
  7. Date & Datetime (avec timezones)
  8. f-strings et formatage
  9. Logging plutôt que print
  10. Type hints & mypy basics
  11. Librairies stdlib utilisées massivement en Odoo

1. Python 3.10+ — Ce qui est nouveau et utile

Odoo 19 exige Python 3.10 minimum. Profitez-en pour utiliser les nouveautés modernes.

Type unions avec |

# AVANT (Python < 3.10)
from typing import Union, Optional
def get_value(x: Union[int, str]) -> Optional[str]: ...

# MAINTENANT (Python ≥ 3.10) — bien plus lisible
def get_value(x: int | str) -> str | None: ...

Pattern matching (match/case)

def handle_state(state: str):
    match state:
        case 'draft':
            return "Pas encore validé"
        case 'available' | 'reserved':           # OR explicite
            return "En catalogue"
        case 'borrowed' if overdue:           # avec garde
            return "En retard !"
        case _:
            return "Inconnu"
💡
Le code Odoo natif n'utilise quasi jamais match

Pour rester homogène avec les modules officiels, préférez if/elif classique. Mais c'est bon de connaître match pour ses scripts perso.

2. Classes & héritage — le coeur d'Odoo

Tout modèle Odoo est une classe Python qui hérite de models.Model. Maîtriser l'héritage Python = comprendre Odoo.

class Animal:
    def __init__(self, name: str):
        self.name = name

    def speak(self) -> str:
        return "..."

class Dog(Animal):                 # héritage simple
    def speak(self) -> str:
        return "Woof"

class RobotDog(Dog):              # chaîne d'héritage
    def speak(self) -> str:
        original = super().speak()    # appel parent
        return f"[BEEP] {original}"

Multi-inheritance & MRO

Odoo utilise massivement le multi-inheritance via _inherit = [...]. Sous le capot, c'est du multi-inheritance Python classique avec son Method Resolution Order.

class MailMixin:
    def notify(self):
        return "email envoyé"

class Activity:
    def notify(self):
        return "activité créée"

class Order(MailMixin, Activity):
    def confirm(self):
        return self.notify()

# Order.__mro__ = [Order, MailMixin, Activity, object]
# → MailMixin.notify() est appelé en premier

@classmethod vs @staticmethod

class Book:
    def read(self):           # méthode d'instance (self)
        return self.title

    @classmethod
    def from_isbn(cls, isbn):    # constructeur alternatif (cls)
        return cls(title=fetch_title(isbn))

    @staticmethod
    def is_valid_isbn(isbn):    # utilitaire pur (ni self ni cls)
        return len(isbn) in (10, 13)
📌
En Odoo : @api.model remplace @classmethod

Une méthode Odoo qui ne dépend pas d'un recordset spécifique se décore avec @api.model (et non @classmethod). Sémantiquement c'est proche, techniquement c'est différent (la méthode reçoit self, mais self est vide).

3. Décorateurs — magie d'Odoo

Un décorateur, c'est juste une fonction qui modifie une autre fonction. Vu comme ça, plus de mystère.

def timing(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        print(f"{func.__name__} a pris {time.time() - start:.2f}s")
        return result
    return wrapper

@timing
def slow_function():
    time.sleep(1)

# @timing est équivalent à : slow_function = timing(slow_function)

En Odoo, les décorateurs sont tous des fonctions de odoo.api :

from odoo import api

@api.depends('price', 'quantity')        # déclencheurs recompute
def _compute_total(self): ...

@api.constrains('isbn')               # validation côté serveur
def _check_isbn(self): ...

@api.onchange('partner_id')            # réaction UI form
def _onchange_partner(self): ...

@api.model_create_multi             # override create batch
def create(self, vals_list): ...

4. Compréhensions (list / dict / set)

Vous verrez ça à chaque ligne dans le code Odoo. À maîtriser absolument.

# List comprehension : pythonique, rapide, lisible
prices = [order.amount_total for order in orders]
big_orders = [o for o in orders if o.amount_total > 1000]
formatted = [f"{o.name}: {o.amount_total}€" for o in orders]

# Dict comprehension : transformer une liste en dict
order_by_id = {order.id: order for order in orders}
prices_per_partner = {
    order.partner_id.id: order.amount_total
    for order in orders
    if order.state == 'done'
}

# Set comprehension : dédupliquer + filtrer
unique_emails = {p.email for p in partners if p.email}

# Generator (paresseux, économise la mémoire)
total = sum(o.amount_total for o in orders if o.state == 'done')
# ← pas de [], donc générateur — pas de liste intermédiaire en mémoire
💡
Règle d'or

Si une compréhension dépasse 2 lignes ou imbrique plusieurs for, c'est trop complexe : repassez en boucle for classique. La lisibilité prime sur la concision.

5. Lambda + filter / map

Pas obligatoire mais utilisé partout sur les recordsets Odoo : .filtered(lambda r: ...).

# Lambda = fonction anonyme
square = lambda x: x ** 2
add = lambda a, b: a + b
is_premium = lambda partner: partner.category_id.name == 'Premium'

# En Odoo : presque toujours via .filtered() et .mapped()
active_orders = orders.filtered(lambda o: o.state not in ['cancel', 'draft'])
big_orders = orders.filtered(lambda o: o.amount_total > 1000)
partner_names = orders.mapped(lambda o: o.partner_id.name)
# équivalent court : orders.mapped('partner_id.name')

# Tri custom
sorted_orders = orders.sorted(lambda o: (o.priority, o.date_order))
# Astuce : retourner un tuple = tri multi-critères

6. Gestion des exceptions

try:
    risky_operation()
except ValueError as e:
    _logger.warning("Valeur invalide: %s", e)
except (KeyError, TypeError) as e:           # plusieurs types
    _logger.error("Erreur: %s", e)
except Exception as e:
    _logger.exception("Erreur inattendue")       # log avec traceback
    raise                                       # relancer
else:
    _logger.info("Succès")                       # si pas d'exception
finally:
    cleanup()                                    # toujours exécuté

Les exceptions Odoo

from odoo.exceptions import UserError, ValidationError, AccessError, MissingError

if book.state == 'archived':
    raise UserError(_("Ce livre est archivé."))
# UserError       = erreur métier, message à l'utilisateur (rouge dans UI)
# ValidationError = échec de validation @api.constrains
# AccessError     = pas le droit (sécurité)
# MissingError    = record n'existe pas / supprimé
Ne JAMAIS faire except: pass

Vous masquez des bugs critiques. Toujours capturer une exception spécifique et au minimum la logger. Si vous ne savez pas quoi capturer : utilisez Exception + _logger.exception() + raise.

7. Date & Datetime

L'un des sujets les plus piégeux. Odoo a son propre wrapper pour gérer les timezones proprement.

from datetime import date, datetime, timedelta, timezone
from dateutil.relativedelta import relativedelta
from odoo import fields

# Date du jour
today = date.today()                            # date Python pure
today = fields.Date.today()                     # équivalent Odoo (préférable)

# Datetime — TOUJOURS en UTC en base Odoo
now = datetime.now(timezone.utc)
now = fields.Datetime.now()                     # équivalent Odoo (préférable)

# Manipulation
tomorrow = today + timedelta(days=1)
in_3_months = today + relativedelta(months=3)   # gère bien les mois
last_year = today + relativedelta(years=-1)

# Parsing depuis une string Odoo (format ISO)
d = fields.Date.from_string('2026-05-19')
dt = fields.Datetime.from_string('2026-05-19 14:30:00')

# Formatage
formatted = fields.Date.to_string(today)        # '2026-05-19'

# Conversion UTC → timezone user
local_dt = fields.Datetime.context_timestamp(self, fields.Datetime.now())
⚠️
Piège classique : comparer date et datetime

date(2026, 5, 19) < datetime(2026, 5, 19, 0, 0) lève une TypeError. Toujours comparer des objets du même type. Préférez fields.Date partout sauf besoin précis d'heure.

8. f-strings & formatage

name = "Victor"
qty = 3
price = 12.456

# f-string : la méthode moderne (≥ Python 3.6)
msg = f"Client {name} a commandé {qty} articles à {price:.2f} €"
# → "Client Victor a commandé 3 articles à 12.46 €"

# Expressions Python dans la f-string
total = f"Total: {qty * price:.2f} €"

# Spécificateurs de format utiles
f"{n:,}"        # 1,234,567 (séparateur milliers)
f"{n:.2%}"      # 12.34% (pourcentage)
f"{d:%Y-%m-%d}" # 2026-05-19
f"{text:>20}"   # aligné à droite sur 20 colonnes
📌
En Odoo : f-string interdites pour les messages traduisibles

Utilisez % ou %s avec la fonction _() pour que les traducteurs puissent extraire les chaînes :

raise UserError(_("Marge faible: %.2f %%") % margin)
# ou avec format()
raise UserError(_("Hello {name}").format(name=user.name))

Les f-strings sont OK pour les logs et les chaînes internes non-affichées à l'utilisateur final.

9. Logging — jamais de print

import logging
_logger = logging.getLogger(__name__)

_logger.debug("Détail trace, dev seulement")
_logger.info("Action OK, traçable en prod")
_logger.warning("Attention, comportement inhabituel")
_logger.error("Erreur, mais on continue")
_logger.critical("Erreur grave, données potentiellement corrompues")

# Format style %s (ne calcule pas le message si niveau pas activé)
_logger.info("Création de %s livres pour %s", len(books), partner.name)
# ↑ mieux que _logger.info(f"Création de {len(books)}...") car lazy

# Logger une exception (avec traceback complet)
try:
    ...
except Exception:
    _logger.exception("Échec du traitement de %s", record.id)

10. Type hints

Optionnels mais fortement encouragés. Le code Odoo officiel les utilise de plus en plus depuis la v17.

from typing import Optional        # pré-3.10

def find_book(isbn: str) -> str | None:    # 3.10+
    if not isbn:
        return None
    return fetch_title(isbn)

def total_price(orders: list, currency: str = "EUR") -> float:
    return sum(o.amount for o in orders)

# Types complexes
from typing import Iterable, Callable

def apply_to_all(items: Iterable[int], func: Callable[[int], int]) -> list[int]:
    return [func(x) for x in items]

11. Stdlib utilisée massivement en Odoo

ModuleUsage Odoo
collections.defaultdictAggregations : compter / grouper sans pré-init
collections.OrderedDictPréserver ordre (rarement utile depuis Py3.7)
base64Décoder les Binary fields uploadés
csv / io.StringIOParser des CSV (imports en masse)
jsonStocker des structures complexes en Text field
hashlibHash de fichiers (déduplication attachments)
reRegex pour valider emails, ISBN, formats custom
dateutil (3rd party)relativedelta (mois, années arithmétique)
pytz (3rd party)Timezones
from collections import defaultdict

# Compter les commandes par partenaire
counts = defaultdict(int)
for order in orders:
    counts[order.partner_id.id] += 1

# Grouper par état
groups = defaultdict(list)
for order in orders:
    groups[order.state].append(order)

# JSON stocké en Text field Odoo
import json
order.extra_data = json.dumps({'meta': 'x', 'tags': ['a', 'b']})
data = json.loads(order.extra_data or '{}')

🎯 Quiz de validation

🧪 5 questions rapides

  1. Quelle est la différence entre list comprehension et generator expression ?
  2. Quand utiliser @classmethod vs @staticmethod ?
  3. Pourquoi préférer _logger.info("msg %s", x) à _logger.info(f"msg {x}") ?
  4. Pourquoi ne PAS utiliser de f-string dans un message _("...") traduisible ?
  5. Quelle est la différence entre timedelta(months=1) et relativedelta(months=1) ?

Réponses brèves :

  1. List [...] = matérialise en mémoire ; Generator (...) = paresseux, calcule à la demande, mémoire constante.
  2. classmethod = besoin de cls (héritage, constructeur alternatif) ; staticmethod = utilitaire pur sans état.
  3. Le formatage %s n'est exécuté QUE si le niveau de log est actif (lazy). Avec f-string, le formatage a toujours lieu, même si le log est filtré.
  4. Les traducteurs utilisent un outil qui extrait les chaînes statiques de _("..."). Une f-string contient des expressions Python que l'outil ne peut pas extraire proprement.
  5. timedelta(months=...) n'existe pas (impossible : les mois ont des durées variables). relativedelta(months=1) ajoute proprement 1 mois calendaire (29 février → 31 mars selon contexte).

📚 Pour aller plus loin

📋 Quiz de validation

Accueil Module 0 — Setup Odoo