Solutions — série 01-3 (chaînes de caractères)
Rappel de la règle d'or : on n'ouvre ce fichier qu'après avoir terminé (ou sérieusement séché sur) la série. Compare le raisonnement, pas seulement le code : une solution différente qui marche est souvent valable.
Exercice facile a — prédire l'indexation
Raisonnement — On dessine le train : t r a d i n g, indices 0 à 6 (7 caractères). Chaque ligne se lit mécaniquement : indice positif = depuis la gauche à partir de 0, négatif = depuis la droite à partir de -1. La dernière ligne demande l'indice 7… qui n'existe pas : le dernier est 6.
Solution
# ex-01-3-a.py
word = "trading"
print(word[0]) # t
print(word[-1]) # g
print(word[3]) # d ← t(0) r(1) a(2) d(3) : compter depuis 0 !
print(len(word)) # 7
print(word[7]) # IndexError: string index out of range
Les quatre premières lignes s'affichent, puis le programme s'arrête net sur le traceback de la cinquième.
Pourquoi ça marche — 7 caractères → indices valides 0 à 6 (et -1 à -7). len(word) vaut 7, mais 7 n'est jamais un indice valide d'une chaîne de longueur 7 : c'est le décalage permanent de la numérotation à partir de 0. Remarque aussi que l'erreur n'empêche pas les lignes précédentes de s'exécuter : Python plante à la ligne fautive, pas avant.
Erreur classique sur cet exercice — Répondre a pour word[3] (en comptant à partir de 1), et prédire g pour word[7] « parce que c'est le 7e caractère ». Les deux erreurs sont la même : oublier que tout commence à 0. Le 7e caractère est word[6].
Variante plus difficile — Prédis word[-7] puis word[-8]. (Réponses : t — l'indice négatif le plus lointain est -len(word) — puis IndexError: string index out of range : les négatifs aussi ont une limite, et c'est le même message d'erreur que côté positif.)
Exercice facile b — découper une date
Raisonnement — "2026-07-05" a un format FIXE : positions 0-3 l'année, 4 un tiret, 5-6 le mois, 7 un tiret, 8-9 le jour. Les slices se déduisent en comptant — et on se rappelle que le stop est exclu : l'année s'arrête à l'indice 3 inclus, donc [:4].
Solution
# ex-01-3-b.py
date = "2026-07-05"
year = date[:4] # indices 0..3 : "2026"
month = date[5:7] # indices 5..6 : "07" (le 4 est le tiret)
day = date[8:] # indices 8..9 : "05"
print(f"Le {day}/{month}/{year}")
Sortie :
Le 05/07/2026
Pourquoi ça marche — Le slicing extrait sans modifier : date reste intacte, on fabrique trois nouvelles chaînes. Les bornes ouvertes ([:4], [8:]) évitent de taper 0 et 10 : moins de chiffres, moins d'erreurs. Noter que year, month, day sont des chaînes ("07" garde son zéro !) — parfait pour l'affichage ; il faudrait int() pour calculer.
Erreur classique sur cet exercice — month = date[5:6] → "0" : oublier que le stop est exclu, donc [5:7] pour deux caractères. L'inverse existe aussi : year = date[0:4] écrit date[0:3] → "202". Règle de vérification : la longueur d'un slice vaut stop - start — 2 caractères de mois → un écart de 2.
Variante plus difficile — Obtiens 2026/07/05 en UNE seule méthode, sans slicing. (Réponse : date.replace("-", "/") — replace remplace TOUTES les occurrences d'un coup. Moralité : avant de découper au slice, demande-toi si une méthode fait déjà le travail.)
Exercice facile c — la fiche client
Raisonnement — Trois outils imposés : len() avant/après pour rendre le nettoyage mesurable, strip() pour nettoyer, upper() pour la fiche. La contrainte « stockée dans une variable » vise LE piège de la leçon : si on ne récupère pas le résultat de strip(), rien ne se passe.
Solution
# ex-01-3-c.py
full = " jean DUPONT "
cleaned = full.strip() # on RÉCUPÈRE la nouvelle chaîne
print(f"Avant : {len(full)} caractères — après : {len(cleaned)} caractères")
print(f"Fiche : {cleaned.upper()}")
Sortie :
Avant : 15 caractères — après : 11 caractères
Fiche : JEAN DUPONT
Pourquoi ça marche — full fait 15 caractères (11 utiles + 4 espaces). strip() fabrique une chaîne de 11, stockée dans cleaned ; full n'a pas bougé, ce qui permet justement d'afficher les deux longueurs. upper() s'applique directement dans la f-string : son résultat est utilisé immédiatement, pas besoin de variable intermédiaire ici.
Erreur classique sur cet exercice — Écrire full.strip() seul sur une ligne puis afficher len(full) : les deux comptages donnent 15, et il n'y a AUCUN message d'erreur. C'est l'erreur 3 de la section 6 — la méthode a fabriqué la chaîne nettoyée et le programme l'a jetée. Toujours : cleaned = full.strip().
Variante plus difficile — Affiche la fiche au format Jean Dupont (initiales en majuscules, le reste en minuscules) sans taper le texte en dur. (Réponse : cleaned.title() → Jean Dupont — une cousine de upper() / lower(). Pour UN seul mot, "python".capitalize() → Python, ou à la main : "P" + "python"[1:] — slicing + concaténation, le motif standard pour « modifier » une chaîne immuable.)
Exercice facile d — prédire le slicing
Raisonnement — Quatre règles à appliquer : borne absente = « jusqu'au bout » ; stop exclu ; pas négatif = on recule ; et le slicing ne plante jamais, il se contente de ce qui existe.
Solution
# ex-01-3-d.py
word = "trading"
print(word[:4]) # trad ← indices 0..3
print(word[4:]) # ing ← de l'indice 4 à la fin
print(word[::-1]) # gnidart ← toute la chaîne, à l'envers
print(word[2:50]) # ading ← 50 dépasse : le slice s'arrête à la fin, sans erreur
print(word[5:2]) # ← chaîne VIDE : start après stop, rien à prendre
Pourquoi ça marche — [:4] + [4:] reconstituent exactement word : c'est la propriété du stop exclu. [::-1] parcourt avec un pas de -1, de la fin vers le début. Les deux dernières lignes illustrent la tolérance du slicing : bornes trop grandes → tronquées ; bornes incohérentes → chaîne vide "". Aucun IndexError possible avec un slice.
Erreur classique sur cet exercice — Prédire un IndexError pour word[2:50]. Non : c'est l'INDEXATION (word[50]) qui plante, pas le slicing. La contrepartie de cette tolérance : word[5:2] affiche une ligne vide et le programme continue avec une chaîne vide dans les pattes — un bug silencieux si tes bornes sont fausses.
Variante plus difficile — Prédis word[-3:], word[:-3] et word[::2]. (Réponses : ing — les 3 derniers ; trad — tout SAUF les 3 derniers, les deux slices sont complémentaires ; taig — un caractère sur deux : t(0), a(2), i(4), g(6).)
Exercice moyen a — les deux bugs du crieur
Raisonnement — On déroule sans exécuter. Ligne 2 : word.upper sans parenthèses — pas d'appel, big reçoit la méthode elle-même ; aucun plantage, mais l'affichage sera du charabia (bug silencieux, erreur 4 de la section 6). Ligne 4 : word[len(word)] = word[7] — indice hors limites, IndexError (erreur 1). Le programme affiche donc UNE ligne fausse puis plante.
Solution
Sortie de la version buggée (à savoir prédire) :
Majuscules : <built-in method upper of str object at 0x0000018A1C1D0F00>
Traceback (most recent call last):
File "c:\Users\jtron\Claude\learnpython\shout.py", line 4, in <module>
print(f"Dernière lettre : {word[len(word)]}")
~~~~^^^^^^^^^^^
IndexError: string index out of range
(L'adresse 0x... change à chaque exécution — c'est normal.)
Version corrigée :
# ex-01-3-e.py
word = "trading"
big = word.upper() # bug 1 : les () manquaient
print(f"Majuscules : {big}")
print(f"Dernière lettre : {word[len(word) - 1]}") # bug 2 : le - 1 manquait
Sortie :
Majuscules : TRADING
Dernière lettre : g
Pourquoi ça marche — word.upper() (avec parenthèses) APPELLE la méthode et retourne "TRADING" ; sans parenthèses, on obtient l'objet-méthode lui-même. Et le dernier indice valide d'une chaîne de longueur 7 est 6, soit len(word) - 1 — ou, plus simple et plus robuste, word[-1].
Erreur classique sur cet exercice — Ne trouver que le bug 2 : le bug 1 ne produit aucun traceback, donc il échappe à la relecture rapide. Réflexe à installer : <built-in method ...> dans une sortie = parenthèses oubliées quelque part. Autre classique : « corriger » la ligne 4 en word[6] en dur — ça marche pour "trading", mais ça re-plantera dès que word change ; la correction générale utilise len() ou -1.
Variante plus difficile — Exécute la version buggée : pourquoi la ligne fausse s'affiche-t-elle AVANT le plantage ? (Réponse : Python exécute de haut en bas et ne s'arrête qu'à la première exception — un bug silencieux en amont survit à un plantage en aval. Déjà vu en série 01-2 : un traceback ne montre qu'UN bug à la fois, et jamais les silencieux.)
Exercice moyen b — les initiales
Raisonnement — Chaîne complète : saisie → extraction → transformation → affichage. first[0] prend la première lettre (l'indexation marche sur n'importe quelle chaîne, y compris le retour d'input()), .upper() la met en majuscule quelle que soit la saisie. Le tout se compose directement dans une f-string.
Solution
# ex-01-3-f.py
first = input("Ton prénom ? ")
last = input("Ton nom ? ")
initials = f"{first[0].upper()}. {last[0].upper()}."
print(f"Initiales : {initials}")
Session :
Ton prénom ? ada
Ton nom ? lovelace
Initiales : A. L.
Pourquoi ça marche — first[0] est une chaîne d'un caractère ("a"), et comme toute chaîne elle a les méthodes : "a".upper() → "A". L'enchaînement first[0].upper() se lit de gauche à droite : indexer, puis transformer. Une expression complète a le droit de vivre dans une f-string — mais si elle devient illisible, on la sort dans une variable, comme ici avec initials.
Erreur classique sur cet exercice — first.upper()[0] : correct aussi (majusculer tout puis prendre la première lettre), mais on transforme toute la chaîne pour n'en garder qu'un caractère — ordre moins logique. Vraie erreur classique : first[1] (« la première lettre, c'est l'indice 1 »), qui donne d pour ada — pas de plantage, juste un résultat faux. Et le cas limite : si l'utilisateur appuie juste sur Entrée, first[0] plante avec IndexError — une chaîne vide n'a AUCUN indice valide (⏩ le if du niveau 2 permettra de s'en protéger).
Variante plus difficile — Affiche aussi le nom complet propre : Ada LOVELACE (prénom avec majuscule initiale seulement, nom tout en majuscules), quel que soit ce que l'utilisateur a tapé. (Piste : first.capitalize() ou first[0].upper() + first[1:].lower() — le motif slicing + concaténation, encore lui.)
Exercice moyen c — le slug
Raisonnement — Quatre transformations imposées, mais dans quel ordre ? Le point délicat : supprimer "!" laisse derrière lui l'espace qui le précédait — "Mon Super Article " a maintenant un espace FINAL. Si on remplace les espaces par des tirets AVANT d'avoir strippé cet espace-là, il devient un tiret final impossible à enlever avec nos outils. Donc : supprimer le !, PUIS strip(), puis le reste.
Solution
# ex-01-3-g.py
title = "Mon Super Article !"
slug = title.replace("!", "").strip().lower().replace(" ", "-")
print(slug)
Sortie :
mon-super-article
Ordre qui échoue (à noter dans le journal) :
bad = title.strip().lower().replace(" ", "-").replace("!", "")
print(bad) # mon-super-article- ← tiret final !
Ici strip() intervient trop tôt : quand "!" disparaît ensuite, l'espace devenu tiret est déjà là — et strip() ne peut plus aider, il ne retire que les espaces.
Pourquoi ça marche — Chaque méthode retourne une nouvelle chaîne, sur laquelle la suivante s'applique : replace("!", "") → "Mon Super Article " → strip() → "Mon Super Article" → lower() → "mon super article" → replace(" ", "-") → "mon-super-article". Un enchaînement se débogue en le coupant : affiche le résultat après chaque maillon pour voir où ça déraille.
Erreur classique sur cet exercice — L'ordre strip()-d'abord ci-dessus. Plus sournois : le bug est silencieux — mon-super-article- s'affiche sans erreur, et si on ne compare pas à la sortie attendue on ne voit rien. Autre classique : replace("!") avec un seul argument → TypeError: replace() takes at least 2 positional arguments (1 given) — replace a besoin de l'ancien ET du nouveau texte ("" pour « rien »).
Variante plus difficile — Fais-le sur une saisie utilisateur (input()), et gère aussi les apostrophes : L'été du chat ! doit donner l-ete-du-chat… presque : é n'est pas transformable avec nos outils par une seule méthode — il faudrait replace("é", "e") pour chaque lettre accentuée. Fais-le pour é et è, et note la lourdeur : c'est exactement le genre de corvée que les boucles (⏩ niveau 2) et les tables de correspondance (⏩ niveau 3) élimineront.
Exercice difficile a — le palindrome
Raisonnement — Trois étapes découvertes via les indices : (1) une chaîne est un palindrome si elle est égale à son propre retournement — et cleaned[::-1] retourne ; (2) mais la comparaison brute échoue sur "Kayak" (majuscule) ou une phrase (espaces) : il faut normaliser AVANT — lower() puis replace(" ", "") ; (3) == produit directement le True/False demandé, pas besoin de if.
Solution
# ex-01-3-h.py
phrase = input("Ta phrase ? ")
cleaned = phrase.lower().replace(" ", "") # normaliser : casse + espaces
print(cleaned == cleaned[::-1]) # un bool : True ou False
Sessions :
Ta phrase ? Engage le jeu que je le gagne
True
Ta phrase ? python
False
Pourquoi ça marche — cleaned[::-1] fabrique la chaîne lue de droite à gauche ; == compare caractère par caractère et retourne un bool (vu en 01-2 : une comparaison EST une valeur, affichable telle quelle). La normalisation en amont fait que "Engage le jeu que je le gagne" devient "engagelejeuquejelegagne" — 23 caractères parfaitement symétriques. Bonus de robustesse : replace(" ", "") supprime TOUS les espaces, y compris ceux que l'utilisateur a tapés autour — pas besoin de strip() séparé ici.
Erreur classique sur cet exercice — Comparer sans normaliser : "Kayak" == "kayaK"[::-1]… non — phrase == phrase[::-1] sur "Kayak" donne False (K ≠ k pour Python). Autre classique : écrire cleaned[::-1] = cleaned en essayant « d'inverser la variable » → TypeError: 'str' object does not support item assignment — le même message que word[0] = "P" : assigner À un slice, c'est encore modifier la chaîne, interdit. Le retournement est une valeur qu'on compare ou qu'on stocke (cleaned = cleaned[::-1]).
Variante plus difficile — Teste Ésope reste ici et se repose : ta version répond False ! Pourquoi ? (é ≠ e — la normalisation ne couvre pas les accents.) Ajoute .replace("é", "e") à la chaîne de nettoyage et vérifie que la phrase devient True. Gère aussi la virgule de Engage le jeu, que je le gagne avec un replace(",", "") de plus. Chaque caractère parasite coûte un replace — retiens la douleur, les outils des niveaux 2 et 3 la feront disparaître.
Exercice difficile b — découper un email
Raisonnement — Tout part de la position du @ : find("@") la donne pour N'IMPORTE quel email — c'est ce qui interdit les positions en dur. L'utilisateur est le slice AVANT cette position, le domaine le slice APRÈS (attention au + 1 pour sauter le @ lui-même). L'extension : même motif (find + slice), rejoué sur la chaîne du domaine.
Solution
# ex-01-3-i.py
email = input("Ton email ? ")
at_pos = email.find("@") # position du @ : la clé de tout
user = email[:at_pos] # tout ce qui précède le @
domain = email[at_pos + 1:] # tout ce qui suit (+1 : on saute le @)
dot_pos = domain.find(".") # même motif, sur le domaine
extension = domain[dot_pos + 1:]
print(f"Utilisateur : {user}")
print(f"Domaine : {domain}")
print(f"Extension : {extension}")
Session :
Ton email ? ada.lovelace@example.com
Utilisateur : ada.lovelace
Domaine : example.com
Extension : com
Pourquoi ça marche — find("@") retourne 12 pour cet email ; email[:12] s'arrête juste avant le @ (stop exclu — pour une fois, la convention nous arrange), email[13:] reprend juste après. Le point crucial est de chercher le . dans domain et PAS dans email : le premier . de l'email entier est celui de ada.lovelace — le slice sur la mauvaise chaîne donnerait lovelace@example.com comme « extension ».
Erreur classique sur cet exercice — Justement celle-là : email.find(".") au lieu de domain.find(".") — aucun plantage, résultat absurde. Deuxième classique : oublier le + 1 → le domaine s'affiche @example.com. Troisième : taper email[13:] en dur parce que « ça marche » avec l'exemple — teste avec bob@x.fr : find s'adapte, les positions en dur non.
Variante plus difficile — Teste avec bob@mail.example.co.uk : ton programme affiche Extension : example.co.uk — le PREMIER point n'est pas le bon ici, il faudrait le DERNIER. Cherche rfind (« find depuis la droite », une cousine de find) et corrige : domain[domain.rfind(".") + 1:] → uk. Moralité : find ↔ premier, rfind ↔ dernier — choisis selon le problème.
Mini-projet — « nettoyeur de texte »
Raisonnement — L'architecture habituelle : saisie → nettoyage → calculs → affichage. Les contraintes dictent la structure : cleaned et low calculées UNE fois (immutabilité oblige, chaque appel refabriquerait la chaîne), les comptages en variables nommées, et un bloc d'affichage où CHAQUE étiquette passe par {'...':<12} pour former la colonne. Le compteur de mots sans boucle est l'astuce à trouver : N mots séparés par des espaces simples = N-1 espaces, donc count(" ") + 1.
Solution
# cleaner.py
print("=== Nettoyeur de texte ===")
raw = input("Ta phrase ? ")
# --- cleaning ---------------------------------------------------------------
cleaned = raw.strip() # une seule fois, réutilisée partout
low = cleaned.lower() # idem
# --- counts -----------------------------------------------------------------
char_count = len(cleaned)
word_count = cleaned.count(" ") + 1
vowel_count = (low.count("a") + low.count("e") + low.count("i")
+ low.count("o") + low.count("u") + low.count("y"))
# --- report -----------------------------------------------------------------
print(f"{'Originale':<12}: [{raw}]")
print(f"{'Nettoyée':<12}: [{cleaned}]")
print(f"{'MAJUSCULES':<12}: {cleaned.upper()}")
print(f"{'minuscules':<12}: {low}")
print(f"{'Inversée':<12}: {cleaned[::-1]}")
print(f"{'Caractères':<12}: {char_count:>4}")
print(f"{'Mots':<12}: {word_count:>4}")
print(f"{'Voyelles':<12}: {vowel_count:>4}")
Session (l'utilisateur a tapé des espaces avant ET après) :
=== Nettoyeur de texte ===
Ta phrase ? le Python est un langage GÉNIAL
Originale : [ le Python est un langage GÉNIAL ]
Nettoyée : [le Python est un langage GÉNIAL]
MAJUSCULES : LE PYTHON EST UN LANGAGE GÉNIAL
minuscules : le python est un langage génial
Inversée : LAINÉG egagnal nu tse nohtyP el
Caractères : 31
Mots : 6
Voyelles : 10
(Le calcul des voyelles est écrit sur deux lignes : à l'intérieur de parenthèses ouvertes, Python accepte le passage à la ligne — pratique pour les longues additions.)
Pourquoi ça marche — cleaned compte 31 caractères (les espaces INTÉRIEURS restent : strip() ne touche que les bouts) ; 5 espaces → 6 mots. Les voyelles sont comptées sur low : un seul passage en minuscules sert aux six count() ET à l'affichage — c'est le bénéfice de stocker. Côté format : {'Originale':<12} cale l'étiquette à gauche sur 12 caractères, d'où des : parfaitement alignés ; {char_count:>4} cale les nombres à droite — les unités sous les unités.
Remarque honnête sur la session : Voyelles : 10 alors qu'on voit 11 voyelles ? Non — le É de GÉNIAL devient é en minuscules, et count("e") ne compte PAS é (caractères différents pour Python, comme dans le palindrome d'Ésope). Limite connue, assumée ici.
Erreur classique sur cet exercice — Écrire raw.strip() puis raw.lower() sans rien récupérer : tout l'affichage sort brut, avec les espaces — aucun message d'erreur (LE piège de la leçon, encore). Deuxième classique : compter les voyelles sur cleaned au lieu de low — les majuscules échappent aux count() en minuscules, le total est faux, en silence. Troisième : {'Nettoyée:<12'} (le format DANS les apostrophes) — la mise en forme s'affiche littéralement au lieu de s'appliquer ; le :<12 va après l'apostrophe fermante, avant le }.
Variante plus difficile — (1) L'extension de l'énoncé : affiche Espaces doubles : {' ' in cleaned} et teste avec deux espaces entre deux mots — le compteur annonce un mot de trop (le chat dort → 4 « mots ») : count(" ") + 1 compte les espaces, pas les mots. ⏩ Au niveau 3, len(cleaned.split()) réglera ça proprement (split() sans argument fusionne les espaces multiples). (2) Ajoute une ligne Signes qui compte ., ,, ! et ? (quatre count()). (3) ⏩ Au niveau 2, un if affichera un avertissement quand la phrase est vide — aujourd'hui, une saisie vide donne Mots : 1, et tu sais maintenant exactement pourquoi.