Tutoriels en français

Lire un CSV « à la française » avec pandas : point-virgule, virgule décimale, encodage et codes postaux

Tutoriels en français · · Publié le 2026-11-09

Meuble de fichier en bois à nombreux petits tiroirs étiquetés, dans une bibliothèque publique
Des fiches bien rangées : c'est ce qu'on attend d'un fichier CSV, et pas toujours ce qu'on reçoit.

Un fichier CSV enregistré par Excel réglé en français n'a pas grand-chose à voir avec le CSV que pandas attend par défaut : les colonnes sont séparées par des points-virgules, les décimales par des virgules, les milliers par des espaces insécables, le texte est souvent encodé en Windows-1252 et les dates s'écrivent JJ/MM/AAAA. Sans réglage, pd.read_csv() échoue ou, pire, lit sans erreur des montants sous forme de texte et des codes postaux amputés de leur zéro. Voici les paramètres qui règlent chaque problème, testés un par un.

Exemples exécutés avec Python 3.13 et pandas 3.0.5 ; les sorties sont réelles. Les blocs s'enchaînent dans un même fichier.

Le fichier d'exemple

Pour reproduire fidèlement un export de tableur, on écrit le fichier octet par octet : encodage Windows-1252 (cp1252), fins de ligne Windows, espaces insécables comme séparateurs de milliers. Les données sont fictives. Les colonnes réunissent les pièges habituels : un code commune corse (2A004), un code postal de l'Ain qui commence par 0, des montants avec décimales et milliers.

lignes = [
    "date_facture;client;code_commune;code_postal;montant_ttc;tva",
    "05/01/2026;Boulangerie Sénéchal;01053;01000;1\u00a0234,50;205,75",
    "12/01/2026;Café des Îles;2A004;20000;89,90;14,98",
    "03/02/2026;Atelier Durand;75056;75011;12\u00a0000,00;2\u00a0000,00",
]
with open("factures.csv", "wb") as f:
    f.write(("\r\n".join(lignes) + "\r\n").encode("cp1252"))

Premier essai : l'erreur d'encodage, puis une seule colonne

Lu sans paramètre, le fichier provoque une UnicodeDecodeError : pandas suppose de l'UTF-8 et tombe sur l'octet 0xe9, qui code « é » en Windows-1252. Une fois l'encodage indiqué, le fichier se lit, mais en une seule colonne, puisque pandas cherche des virgules :

import pandas as pd

try:
    pd.read_csv("factures.csv")
except UnicodeDecodeError as err:
    print("UnicodeDecodeError :", err)

df = pd.read_csv("factures.csv", encoding="cp1252")
print(df.shape)
print(df.columns.tolist())
UnicodeDecodeError : 'utf-8' codec can't decode byte 0xe9 in position 86: invalid continuation byte
(3, 1)
['date_facture;client;code_commune;code_postal;montant_ttc;tva']

Si vous ne savez pas quel encodage employer, notre tutoriel en anglais sur UnicodeDecodeError avec pandas explique comment le deviner. Pour les fichiers français, la règle pratique est simple : essayer utf-8, puis cp1252. Préférez cp1252 à latin-1 : les deux coïncident pour les lettres accentuées, mais seul le premier décode correctement l'euro (€), l'apostrophe typographique et le « œ ».

Le séparateur ne suffit pas

Avec sep=";", les colonnes apparaissent. Regardez pourtant les types :

df = pd.read_csv("factures.csv", sep=";", encoding="cp1252")
print(df[["code_commune", "code_postal", "montant_ttc"]])
print(df.dtypes)
  code_commune  code_postal montant_ttc
0        01053         1000    1 234,50
1        2A004        20000       89,90
2        75056        75011   12 000,00
date_facture      str
client            str
code_commune      str
code_postal     int64
montant_ttc       str
tva               str
dtype: object

Trois problèmes passent inaperçus si l'on ne vérifie pas dtypes :

Le Code officiel géographique de l'Insee définit le code commune sur cinq caractères, dont les deux premiers donnent le département (2A et 2B pour la Corse). Ces codes, comme les codes postaux, les SIREN et les numéros de téléphone, sont des identifiants : ils doivent toujours être lus en texte, jamais en nombres.

La lecture complète, réglage par réglage

df = pd.read_csv(
    "factures.csv",
    sep=";",
    encoding="cp1252",
    decimal=",",
    thousands="\u00a0",          # espace insécable
    dtype={"code_commune": "string", "code_postal": "string"},
    parse_dates=["date_facture"],
    date_format="%d/%m/%Y",
    engine="python",
)
print(df[["date_facture", "code_postal", "montant_ttc", "tva"]])
print("Total TTC :", df["montant_ttc"].sum())
  date_facture code_postal  montant_ttc      tva
0   2026-01-05       01000       1234.5   205.75
1   2026-01-12       20000         89.9    14.98
2   2026-02-03       75011      12000.0  2000.00
Total TTC : 13324.4

Le moteur C de pandas, utilisé par défaut, ignore en effet un séparateur de milliers qui n'est pas un caractère ASCII. Sans engine="python", aucune erreur n'est levée, mais les montants restent du texte :

sans_moteur = pd.read_csv("factures.csv", sep=";", encoding="cp1252",
                          decimal=",", thousands="\u00a0")
print(sans_moteur["montant_ttc"].tolist())
['1\xa0234,50', '89,90', '12\xa0000,00']

Le moteur Python est plus lent : pour un fichier de plusieurs centaines de mégaoctets, la solution suivante est préférable.

Espaces fines et formats mixtes : un convertisseur

Selon le logiciel et sa version, le séparateur de milliers peut être une espace ordinaire, une espace insécable (U+00A0) ou une espace fine insécable (U+202F), et un même fichier assemblé à la main peut mélanger les trois. Le paramètre thousands n'accepte qu'un caractère ; un convertisseur par colonne règle tous les cas et fonctionne avec le moteur C :

def montant_fr(texte: str) -> float:
    """'1 234,50' avec espace, espace insécable ou espace fine -> 1234.5"""
    for espace in (" ", "\u00a0", "\u202f"):
        texte = texte.replace(espace, "")
    return float(texte.replace(",", "."))

print(montant_fr("2\u202f113\u202f705,40"))

df = pd.read_csv(
    "factures.csv", sep=";", encoding="cp1252",
    dtype={"code_commune": "string", "code_postal": "string"},
    converters={"montant_ttc": montant_fr, "tva": montant_fr},
)
print(df[["client", "montant_ttc", "tva"]])
2113705.4
                 client  montant_ttc      tva
0  Boulangerie Sénéchal       1234.5   205.75
1         Café des Îles         89.9    14.98
2        Atelier Durand      12000.0  2000.00

Un convertisseur s'applique valeur par valeur ; si une cellule est vide ou contient « n.d. », il lèvera une erreur. Ajoutez alors un test en tête de fonction et renvoyez float("nan") pour les valeurs manquantes.

Détecter le séparateur automatiquement

Quand vous recevez des fichiers de sources variées, certains avec des virgules, d'autres avec des points-virgules ou des tabulations, sep=None laisse le module csv deviner le séparateur à partir du début du fichier (moteur Python obligatoire) :

auto = pd.read_csv("factures.csv", sep=None, engine="python", encoding="cp1252")
print(auto.shape)
(3, 6)

La détection est fiable sur des fichiers réguliers, moins sur un fichier dont la première ligne contient du texte libre avec des virgules. Pour un traitement automatisé, fixez le séparateur dès que la source est connue.

Réexporter pour Excel en français

Dans l'autre sens, un CSV encodé en UTF-8 sans marque d'ordre des octets (BOM) s'ouvre avec des accents cassés dans Excel sous Windows, et un CSV à virgules s'ouvre sur une seule colonne. L'encodage utf-8-sig ajoute le BOM qui permet à Excel de reconnaître l'UTF-8 :

df["date_facture"] = pd.to_datetime(df["date_facture"], format="%d/%m/%Y")
df.to_csv("export.csv", sep=";", decimal=",", index=False, encoding="utf-8-sig",
          date_format="%d/%m/%Y", float_format="%.2f")

with open("export.csv", encoding="utf-8-sig") as f:
    print(f.read())
date_facture;client;code_commune;code_postal;montant_ttc;tva
05/01/2026;Boulangerie Sénéchal;01053;01000;1234,50;205,75
12/01/2026;Café des Îles;2A004;20000;89,90;14,98
03/02/2026;Atelier Durand;75056;75011;12000,00;2000,00

Le fichier conserve les zéros des codes postaux et des codes commune, puisque ce sont des chaînes. Pour qu'Excel les affiche sans les convertir en nombres à l'ouverture, passez par l'import de données (onglet Données, « À partir d'un fichier texte/CSV ») et déclarez ces colonnes en texte. float_format="%.2f" garde deux décimales sur les montants.

Récapitulatif

pd.read_csv(chemin, sep=";", encoding="cp1252", decimal=",", thousands="\u00a0",
            dtype={"code_postal": "string", "code_commune": "string"},
            parse_dates=["date"], date_format="%d/%m/%Y", engine="python")

Ces réglages couvrent la grande majorité des exports de tableurs français. Les fichiers publiés sur data.gouv.fr par les administrations sont, eux, de plus en plus souvent en UTF-8 avec des virgules : vérifiez les premières lignes avant de choisir. Une fois les données propres, vous pouvez par exemple contrôler une colonne de numéros SIRET, marquer les jours fériés dans une série de dates ou géocoder une colonne d'adresses.

Sources