Vue d'ensemble

Python 3.6, publié le 23 décembre 2016, est une version majeure qui introduit certaines des fonctionnalités les plus appréciées du Python moderne. Les f-strings révolutionnent le formatage de chaînes, les annotations de variables étendent le système de typage, et le module secrets apporte une solution sécurisée pour la génération de jetons.

Cette version marque un tournant dans l'adoption de Python 3 : avec des améliorations aussi pratiques au quotidien, de nombreux projets ont enfin franchi le pas de la migration depuis Python 2. Les générateurs asynchrones et les soulignés dans les littéraux numériques complètent un ensemble de nouveautés remarquable.

Nouveautés majeures

f-strings (PEP 498)

Les f-strings (littéraux de chaînes formatés) constituent probablement l'ajout le plus populaire de Python 3.6. En préfixant une chaîne par f, on peut y insérer directement des expressions Python entre accolades. C'est plus concis, plus lisible et plus rapide que str.format() ou le formatage %.

python
# Formatage de base avec des f-strings
nom = "Alice"
age = 32
print(f"Je m'appelle {nom} et j'ai {age} ans.")
# Je m'appelle Alice et j'ai 32 ans.

# Expressions dans les f-strings
prix_ht = 149.99
tva = 0.20
print(f"Prix TTC : {prix_ht * (1 + tva):.2f} €")
# Prix TTC : 179.99 €

# Appel de fonctions et méthodes
inventaire = ["clavier", "souris", "écran", "casque"]
print(f"Articles en stock : {len(inventaire)}")
# Articles en stock : 4
print(f"Premier article : {inventaire[0].upper()}")
# Premier article : CLAVIER

# Comparaison avec les anciennes méthodes
ville = "Lyon"
population = 516092

# Ancien style (%)
print("%s : %d habitants" % (ville, population))
# Style .format()
print("{} : {} habitants".format(ville, population))
# f-string (le plus lisible)
print(f"{ville} : {population:,} habitants")
# Lyon : 516,092 habitants

# f-strings imbriquées
largeur = 12
valeur = 3.14159
print(f"{f'{valeur:.4f}':>{largeur}}")
#       3.1416

# Dictionnaires dans les f-strings
serveur = {"hote": "192.168.1.10", "port": 8080}
print(f"Connexion à {serveur['hote']}:{serveur['port']}")
# Connexion à 192.168.1.10:8080

Annotations de variables (PEP 526)

Python 3.6 introduit une syntaxe pour annoter le type des variables, en complément des annotations de fonctions déjà présentes. Ces annotations n'ont pas d'effet à l'exécution, mais elles améliorent la documentation du code et permettent aux outils d'analyse statique comme mypy de détecter des erreurs de type.

python
# Annotations de variables simples
nom: str = "Inspyration"
version: int = 3
active: bool = True
ratio: float = 0.85

# Annotations avec types complexes
from typing import List, Dict, Optional

extensions: List[str] = [".py", ".pyx", ".pyi"]
compteurs: Dict[str, int] = {"visites": 0, "erreurs": 0}
resultat: Optional[str] = None

# Variables de classe annotées
class Projet:
    nom: str
    version: str
    dependances: List[str]

    def __init__(self, nom: str, version: str) -> None:
        self.nom = nom
        self.version = version
        self.dependances = []

# Classe de configuration typée
class ConfigBDD:
    hote: str = "localhost"
    port: int = 5432
    nom_base: str = "app_db"
    pool_min: int = 2
    pool_max: int = 10
    ssl_actif: bool = False

config = ConfigBDD()
print(f"Connexion à {config.hote}:{config.port}/{config.nom_base}")
# Connexion à localhost:5432/app_db

# Les annotations sont accessibles via __annotations__
print(ConfigBDD.__annotations__)
# {'hote': <class 'str'>, 'port': <class 'int'>, ...}

Module secrets

Le module secrets fournit des fonctions pour générer des données aléatoires cryptographiquement sûres. Contrairement au module random qui utilise un générateur pseudo-aléatoire (prévisible si on connaît la graine), secrets utilise les sources d'entropie du système d'exploitation et convient à la génération de mots de passe, jetons d'authentification et URL sécurisées.

python
import secrets
import string

# Générer un jeton hexadécimal (idéal pour les clés API)
jeton_hex = secrets.token_hex(32)
print(f"Clé API : {jeton_hex}")
# Clé API : a3f1b9c8d4e2... (64 caractères hex)

# Générer un jeton URL-safe (idéal pour les liens de réinitialisation)
jeton_url = secrets.token_urlsafe(48)
print(f"https://example.com/reset?token={jeton_url}")

# Générer un mot de passe robuste
def generer_mot_de_passe(longueur=16):
    """Génère un mot de passe sécurisé avec majuscules,
    minuscules, chiffres et caractères spéciaux."""
    alphabet = string.ascii_letters + string.digits + string.punctuation
    # S'assurer qu'il y a au moins un caractère de chaque catégorie
    while True:
        mdp = ''.join(secrets.choice(alphabet) for _ in range(longueur))
        if (any(c.islower() for c in mdp)
                and any(c.isupper() for c in mdp)
                and any(c.isdigit() for c in mdp)
                and any(c in string.punctuation for c in mdp)):
            return mdp

print(f"Mot de passe : {generer_mot_de_passe()}")
# Mot de passe : k9#Lm2$xPq7&vN!w

# Différence avec random (NE PAS utiliser random pour la sécurité)
import random
# random.choice(alphabet)    # Pseudo-aléatoire, prévisible !
# secrets.choice(alphabet)   # Cryptographiquement sûr

# Comparaison sécurisée de jetons (résistante aux attaques temporelles)
jeton_stocke = secrets.token_hex(16)
jeton_recu = jeton_stocke  # Simule un jeton valide
print(secrets.compare_digest(jeton_stocke, jeton_recu))  # True

Soulignés dans les littéraux numériques

Python 3.6 permet d'utiliser le caractère souligné (_) comme séparateur visuel dans les littéraux numériques. Cela améliore considérablement la lisibilité des grands nombres, des valeurs hexadécimales, binaires et octales.

python
# Grands nombres décimaux
population_mondiale = 7_900_000_000
budget_annuel = 1_250_000.50
print(f"Population : {population_mondiale:_}")  # 7_900_000_000
print(f"Budget : {budget_annuel:,.2f} €")      # 1,250,000.50 €

# Valeurs hexadécimales (couleurs, adresses mémoire)
couleur_fond = 0xFF_CC_00    # Jaune
masque_reseau = 0xFF_FF_FF_00
print(f"Couleur : #{couleur_fond:06X}")  # Couleur : #FFCC00

# Valeurs binaires (masques de bits, registres)
permissions = 0b_111_101_101   # rwxr-xr-x (755)
registre = 0b_1010_0011_1100_0101
print(f"Permissions : {permissions:o}")  # 755

# Valeurs octales
droits_fichier = 0o_755
droits_dossier = 0o_750

# Comparaison de lisibilité
sans_separateur = 10000000000
avec_separateur = 10_000_000_000
# Le second est clairement 10 milliards

Générateurs asynchrones (PEP 525)

Python 3.6 permet d'utiliser yield dans les fonctions async def, créant ainsi des générateurs asynchrones. Cela comble une lacune importante : on peut désormais produire des valeurs une par une de manière asynchrone, ce qui est idéal pour le streaming de données, la pagination d'API ou la lecture de flux en continu.

python
import asyncio

# Générateur asynchrone : produit des données par lots
async def flux_capteur(nom, nb_mesures=5, delai=0.1):
    """Simule un flux de données d'un capteur IoT."""
    import random
    for i in range(nb_mesures):
        await asyncio.sleep(delai)  # Simule l'attente du capteur
        temperature = 20.0 + random.uniform(-3, 3)
        yield {"capteur": nom, "mesure": i + 1, "temp": round(temperature, 1)}

# Consommation avec async for
async def surveiller_capteurs():
    """Lit les données de plusieurs capteurs."""
    async for donnee in flux_capteur("salle-serveur", nb_mesures=3):
        print(f"[{donnee['capteur']}] Mesure {donnee['mesure']} : "
              f"{donnee['temp']}°C")

# asyncio.run(surveiller_capteurs())
# [salle-serveur] Mesure 1 : 21.3°C
# [salle-serveur] Mesure 2 : 18.7°C
# [salle-serveur] Mesure 3 : 20.1°C

# Générateur asynchrone pour la pagination d'une API
async def paginer_resultats(url_base, taille_page=10):
    """Parcourt les pages d'une API de manière asynchrone."""
    page = 1
    while True:
        # Simule un appel API
        await asyncio.sleep(0.05)
        donnees = [f"element_{(page - 1) * taille_page + i}"
                   for i in range(taille_page)]
        if not donnees or page > 3:  # Limite pour l'exemple
            break
        yield donnees
        page += 1

async def traiter_toutes_les_pages():
    async for lot in paginer_resultats("https://api.example.com/items"):
        print(f"Reçu {len(lot)} éléments : {lot[0]} ... {lot[-1]}")

# asyncio.run(traiter_toutes_les_pages())

Améliorations mineures

  • Les dictionnaires conservent désormais l'ordre d'insertion (détail d'implémentation CPython, garanti à partir de Python 3.7).
  • Nouvelle implémentation du système de formatage des chaînes avec str.__format__.
  • Le module asyncio est stabilisé et n'est plus provisoire.
  • Ajout de os.fspath() et du protocole __fspath__ (PEP 519) pour une meilleure intégration des chemins de fichiers.
  • Le module typing gagne NamedTuple avec syntaxe de classe.
  • Les compréhensions asynchrones et les expressions await dans les compréhensions sont supportées (PEP 530).

Dépréciations et suppressions

Les changements notables :

  • L'encodage par défaut du système de fichiers sous Windows passe à UTF-8 (PEP 529).
  • Le module asynchat est marqué comme déprécié.
  • Le module asyncore est marqué comme déprécié.
  • La constante ssl.PROTOCOL_SSLv23 est renommée en ssl.PROTOCOL_TLS.

Sources