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

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 postal 01000 est devenu 1000, un entier. Tous les départements de 01 à 09 sont touchés, et la jointure avec un autre fichier échouera.
- Le code commune a gardé son zéro, mais par chance : la valeur corse 2A004 a forcé toute la colonne en texte. Sans ligne corse, 01053 serait devenu 1053.
- Les montants sont du texte : impossible de les additionner.
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
decimal=",": la virgule décimale ;thousands="\u00a0": l'espace insécable utilisée par Excel en français pour séparer les milliers ;dtype={…: "string"}: les identifiants restent du texte, zéros compris ;parse_datesetdate_format="%d/%m/%Y": des dates au format jour/mois/année, sans risque d'inversion ;engine="python": indispensable ici, et c'est le piège le moins connu.
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
- pandas, documentation de
pandas.read_csv(paramètressep,decimal,thousands,engine,converters), en anglais. - Insee, Code officiel géographique (structure des codes commune, dont 2A et 2B pour la Corse).
Tutoriels en françaisVérifier un SIRET ou un SIREN
Clé de Luhn, exception de La Poste, numéro de TVA et vérification en ligne : le contrôle complet en quelques lignes de Python.
Tutoriels en françaisJours fériés et jours ouvrés 2026-2030
Les 11 jours fériés (13 en Alsace-Moselle) calculés depuis Pâques, vérifiés avec l'API officielle, et le nombre de jours ouvrés par année.
Tutoriels en françaisGéocoder des adresses avec la Géoplateforme
De l'adresse aux coordonnées et retour, un CSV entier en une requête : l'API de géocodage qui remplace api-adresse.data.gouv.fr.