Leçon 01-3 — Les chaînes de caractères en profondeur

1. Objectif

À la fin de cette leçon tu sauras : accéder à n'importe quel caractère d'une chaîne par sa position (positive ou négative), en extraire des morceaux avec le slicing, mesurer avec len(), transformer avec les méthodes essentielles (upper, lower, strip, replace…), chercher dedans (in, find, count, startswith), et produire des affichages alignés avec les f-strings ({x:.2f}, {x:>10}). Tu sauras aussi expliquer pourquoi une chaîne ne se modifie jamais — et éviter les deux bugs que cette règle provoque.

2. Pourquoi c'est important

Presque toutes les données que ton programme reçoit arrivent en texte : la saisie d'un input(), une ligne de fichier CSV (⏩ niveau 9), une réponse web, un nom de fichier. Avant de les utiliser, il faut les nettoyer (espaces parasites, majuscules incohérentes) et les découper (extraire l'année d'une date, le domaine d'un email). C'est exactement ce que fait cette leçon. Les outils vus ici — indexation, slicing, méthodes — reviendront à l'identique au niveau 3 sur les listes : tout ce que tu apprends ici, tu l'apprends deux fois.

3. Explication simple

Une chaîne, c'est un train de caractères : chaque caractère a son wagon, et chaque wagon a un numéro. La numérotation commence à 0, pas à 1 :

 P   y   t   h   o   n
 0   1   2   3   4   5     ← indices positifs (depuis la gauche)
-6  -5  -4  -3  -2  -1     ← indices négatifs (depuis la droite)

Une chaîne sait aussi faire des choses : elle a des méthodes, qu'on appelle avec un point : word.upper() fabrique la version en majuscules, word.strip() fabrique la version sans espaces autour.

Le mot important est fabrique : une chaîne est immuable — on ne change jamais ses wagons. Toute transformation crée un nouveau train, et c'est à toi de le récupérer dans une variable.

4. Explication approfondie

a) Indexation : les deux sens, et la sortie de route.

Un indice positif compte depuis la gauche à partir de 0 ; un indice négatif compte depuis la droite à partir de -1 (word[-1] est le dernier caractère, très utilisé). Les indices valides de "Python" vont de 0 à 5 et de -1 à -6. En dehors : IndexError: string index out of range. Piège classique : le dernier caractère est word[len(word) - 1], pas word[len(word)] — la numérotation à partir de 0 décale tout d'un cran.

b) Slicing : [start:stop:step], et la règle du stop exclu.

word[start:stop] prend du wagon start inclus au wagon stop exclu. Cette exclusion paraît bizarre mais elle est cohérente : word[0:3] contient exactement 3 caractères (stop - start), et word[:3] + word[3:] recollent la chaîne entière sans doublon. Chaque borne est optionnelle :

Contrairement à l'indexation, le slicing ne plante jamais : word[2:50] s'arrête gentiment à la fin, word[5:2] donne une chaîne vide "". C'est confortable — et sournois : une borne fausse ne produit aucune erreur, juste un résultat faux. Vérifie ta sortie.

c) Immutabilité : la règle qui gouverne toute la leçon.

Une chaîne ne se modifie jamais en place. Deux conséquences concrètes :

word = "python"
word[0] = "P"      # TypeError: 'str' object does not support item assignment

et — bien plus fréquent car silencieux :

message = "bonjour"
message.upper()      # fabrique "BONJOUR"... et le jette aussitôt !
print(message)       # bonjour  ← inchangé

Toute méthode de chaîne retourne une nouvelle chaîne ; l'originale reste intacte. Le réflexe : toujours récupérer le résultat — message = message.upper(). Ce n'est pas une bizarrerie gratuite : une valeur immuable ne peut pas changer dans ton dos, ce qui rend les programmes plus prévisibles. Au niveau 3, tu rencontreras les listes, qui sont mutables — et le contraste te fera comprendre pourquoi c'est un vrai sujet.

d) Les méthodes : des fonctions attachées à la valeur.

upper() se colle après la valeur avec un point : word.upper(). Les parenthèses sont obligatoires — sans elles, tu obtiens la méthode elle-même, pas son résultat (bug silencieux, voir section 6). Comme chaque méthode retourne une chaîne, on peut enchaîner : raw.strip().lower() se lit de gauche à droite (d'abord strip, puis lower sur le résultat). L'ordre compte — tu le vérifieras en exercice.

Les indispensables : upper() / lower() (casse), strip() (enlève espaces et retours à la ligne aux deux bouts, pas au milieu), replace(old, new) (remplace toutes les occurrences), startswith(prefix) (retourne un bool), count(sub) (nombre d'occurrences), find(sub) (position de la première occurrence, ou -1 si absent — jamais d'erreur, c'est un piège, voir section 6).

e) Chercher : in d'abord, find si tu as besoin de la position.

"py" in sentence retourne True/False (un bool, vu en 01-2) et se lit comme de l'anglais : c'est l'outil par défaut. find sert uniquement quand tu veux — pour découper ensuite avec un slice. Tout est sensible à la casse : "Py" in "python" est False ; d'où le motif classique ma_chaine.lower() avant de chercher.

f) split et join — aperçu honnête.

"a,b,c".split(",") découpe en liste ['a', 'b', 'c'], et "-".join(liste) recolle. ⏩ Les listes sont le grand sujet du niveau 3 : ici je te les montre une fois parce que split/join sont inséparables des chaînes, mais on ne s'en servira pas dans les exercices de cette leçon.

g) f-strings avancées : le mini-langage de format.

Après les deux-points dans {...}, tu décris l'affichage : {x:.2f} (2 décimales, vu en 01-2), {x:>10} (aligné à droite sur 10 caractères), {x:<10} (à gauche), {x:^10} (centré). Combinables : {price:>8.2f} = « 2 décimales, aligné à droite sur 8 ». C'est l'outil des tableaux propres dans le terminal — le mini-projet t'en fera faire un.

Note sur les boucles : parcourir une chaîne caractère par caractère se fait avec for — ⏩ c'est LE sujet du niveau 2 ; je l'évite entièrement ici car len, count, in et le slicing couvrent tous nos besoins sans boucle, et tu apprendras le parcours proprement là-bas.

Difficulté honnête : cette leçon est dense — deux vrais pièges conceptuels (le stop exclu du slicing, l'immutabilité) et beaucoup de vocabulaire de méthodes. Prévois deux sessions : sections 1–7 en tapant chaque exemple, puis les exercices. Les méthodes ne s'apprennent pas par cœur : elles rentrent par les doigts.

5. Exemples commentés

Exemple 1 — indexation et len()

word = "Python"
print(word[0])              # P  ← premier caractère : indice 0, pas 1 !
print(word[5])              # n  ← 6 caractères, donc dernier indice = 5
print(word[-1])             # n  ← -1 : le dernier, sans connaître la longueur
print(word[-6])             # P  ← -6 : le premier, en comptant depuis la droite
print(len(word))            # 6
print(word[len(word) - 1])  # n  ← formule générale du dernier caractère

Exemple 2 — slicing dans tous les sens

word = "Python"
print(word[0:2])    # Py      ← indices 0 et 1 : le 2 est EXCLU
print(word[2:])     # thon    ← de l'indice 2 à la fin
print(word[:3])     # Pyt     ← du début à l'indice 2
print(word[-3:])    # hon     ← les 3 derniers caractères
print(word[::2])    # Pto     ← un caractère sur deux (pas de 2)
print(word[::-1])   # nohtyP  ← pas de -1 : la chaîne à l'envers
print(word[2:50])   # thon    ← borne trop grande : pas d'erreur, ça s'arrête

Exemple 3 — nettoyer : strip, upper, lower, replace

raw = "  Bonjour Python  "
print(raw.strip())                      # Bonjour Python   ← espaces des bouts enlevés
print(raw.strip().upper())              # BONJOUR PYTHON   ← enchaînement : strip PUIS upper
print(raw.strip().lower())              # bonjour python
print(raw.replace("Bonjour", "Hello"))  #   Hello Python   ← les espaces restent :
                                        # replace ne touche que ce qu'on lui demande

Chaque ligne fabrique une nouvelle chaîne : raw n'a pas bougé d'un caractère entre la première et la dernière ligne.

Exemple 4 — chercher : in, startswith, count, find

sentence = "le python est un langage, le python est lisible"
print("python" in sentence)       # True   ← le test d'appartenance, outil par défaut
print("java" in sentence)         # False
print(sentence.startswith("le"))  # True   ← sensible à la casse ("Le" → False)
print(sentence.count("python"))   # 2      ← nombre d'occurrences
print(sentence.find("est"))       # 10     ← position de la PREMIÈRE occurrence
print(sentence.find("java"))      # -1     ← absent : -1, PAS une erreur
print(sentence[10:13])            # est    ← find + slice : le duo qui découpe

Exemple 5 — split et join (aperçu ⏩ niveau 3)

csv_line = "ada,1815,london"
parts = csv_line.split(",")   # découpe sur les virgules → une LISTE
print(parts)                  # ['ada', '1815', 'london']
print(parts[0])               # ada  ← l'indexation marche aussi sur les listes !
print("-".join(parts))        # ada-1815-london  ← recolle avec le séparateur choisi

⏩ Les listes ([...]) sont le cœur du niveau 3. Retiens juste l'existence du duo split/join — tu le réclameras bientôt.

Exemple 6 — f-strings avancées : un tableau aligné

print(f"{'article':<12}{'prix':>8}")   # texte à gauche sur 12, à droite sur 8
print(f"{'café':<12}{1.9:>8.2f}")      # >8.2f : 2 décimales, calé à droite sur 8
print(f"{'sandwich':<12}{4.5:>8.2f}")
print(f"{'total':<12}{6.4:>8.2f}")

Sortie :

article         prix
café            1.90
sandwich        4.50
total           6.40

Remarque : dans {'article':<12}, la valeur formatée est une chaîne littérale entre apostrophes — obligatoire ici, car la f-string elle-même utilise déjà les guillemets doubles.

6. Erreurs fréquentes

1) Sortir du train : indice hors limites

word = "Python"
print(word[6])
# IndexError: string index out of range

6 caractères → indices 0 à 5. word[6] n'existe pas, et word[len(word)] non plus (c'est la même erreur déguisée). Le dernier caractère est word[len(word) - 1], ou plus simplement word[-1].

2) Essayer de modifier une chaîne

word = "python"
word[0] = "P"
# TypeError: 'str' object does not support item assignment

Les chaînes sont immuables : on ne remplace pas un wagon. Correction : on fabrique la bonne chaîne — word = "P" + word[1:] (ou word.replace(...), ou word.capitalize() selon le besoin).

3) Appeler une méthode et jeter le résultat

message = "bonjour"
message.upper()          # fabrique "BONJOUR"... que personne ne récupère
print(message)           # bonjour   ← inchangé, aucun message d'erreur

LE bug silencieux de cette leçon. upper() retourne une nouvelle chaîne ; sans assignation, elle disparaît. Correction : message = message.upper().

4) Oublier les parenthèses de la méthode

word = "trading"
big = word.upper                 # ← il manque les ()
print(f"Majuscules : {big}")
# Majuscules : <built-in method upper of str object at 0x0000018A1C1D0F00>

Sans parenthèses, tu obtiens la méthode elle-même (l'adresse hexadécimale varie à chaque exécution), pas son résultat. Pas de plantage : le programme « marche » et affiche du charabia. Dès que tu vois <built-in method ...> dans ta sortie, cherche les parenthèses manquantes.

5) Utiliser le -1 de find() comme un indice

s = "abc"
position = s.find("z")   # "z" absent → -1, sans erreur
print(s[position])       # c   ← s[-1] est VALIDE : le dernier caractère !

Double piège : find ne plante jamais (il retourne -1), et -1 est un indice légal (le dernier caractère). Résultat : aucun message, juste une valeur fausse qui se propage. Réflexe : après un find, demande-toi toujours « et si c'est -1 ? » (⏩ le if du niveau 2 permettra de s'en protéger).

6) Confondre fonction et méthode : .len() n'existe pas

word = "python"
print(word.len())
# AttributeError: 'str' object has no attribute 'len'

len() est une fonction : elle enveloppe la valeur — len(word). Les méthodes (word.upper()) se collent après avec un point. AttributeError signifie « cette valeur n'a pas de méthode portant ce nom » — souvent une faute de frappe (word.uper()) ou une confusion fonction/méthode comme ici.

7. Lire les messages d'erreur

Les deux exceptions emblématiques de cette leçon. Toujours de bas en haut.

IndexError — l'indice hors limites

Traceback (most recent call last):
  File "c:\Users\jtron\Claude\learnpython\badge.py", line 2, in <module>
    print(word[6])
          ~~~~^^^
IndexError: string index out of range
  1. Dernière ligne : IndexError = « tu as demandé un wagon qui n'existe pas ». Le message est toujours le même pour les chaînes : string index out of range.
  2. Au-dessus : fichier badge.py, ligne 2, et le code fautif — les ^^^ soulignent le [6], les ~~~~ la chaîne indexée.
  3. Réflexe : affiche len(word) juste avant la ligne fautive. Neuf fois sur dix, c'est un décalage de 1 (len(word) au lieu de len(word) - 1) ou une chaîne plus courte que prévu (vide, par exemple — un input() où l'utilisateur a juste appuyé sur Entrée a une longueur de 0, et TOUT indice y est hors limites).

TypeError — la modification interdite

Traceback (most recent call last):
  File "c:\Users\jtron\Claude\learnpython\fix.py", line 2, in <module>
    word[0] = "P"
    ~~~~^^^
TypeError: 'str' object does not support item assignment
  1. Dernière ligne : TypeError, comme en 01-2 (« opération interdite pour ce type ») — mais le message est nouveau : does not support item assignment = « on n'assigne pas dans les cases d'un str ». C'est l'immutabilité qui te parle.
  2. Au-dessus : la ligne exacte, avec le [0] souligné.
  3. Réflexe : ne cherche pas à contourner — reformule. Tu ne modifies pas une chaîne, tu en construis une nouvelle (slicing + concaténation, ou replace), puis tu réassignes la variable.

Et rappelle-toi la section 6 : les pires bugs de cette leçon (strip() non assigné, parenthèses oubliées, find → -1) ne produisent aucun traceback. Quand la sortie est fausse sans erreur, relis tes lignes de transformation : le résultat est-il récupéré quelque part ?

8. Exercices — faciles

Règles : nouveau fichier par exercice dans lessons/level-01-bases/exercices/mes-reponses/, nommé ex-01-3-a.py, etc. Tape tout à la main. Prédis la sortie AVANT d'exécuter.

a) Sans exécuter, prédis la sortie de chaque ligne (la dernière est piégée), puis vérifie :

word = "trading"
print(word[0])
print(word[-1])
print(word[3])
print(len(word))
print(word[7])

b) La variable date = "2026-07-05" est au format année-mois-jour. Extrais l'année, le mois et le jour avec trois slices, puis affiche en f-string : Le 05/07/2026. Contrainte : interdiction de retaper les nombres en dur.

c) Crée full = " jean DUPONT " (avec les espaces). Affiche : Avant : 15 caractères — après : 11 caractères puis Fiche : JEAN DUPONT. Contraintes : strip(), upper() et len() obligatoires ; la version nettoyée est stockée dans une variable (pas re-calculée à chaque ligne).

d) Toujours avec word = "trading", prédis puis vérifie :

print(word[:4])
print(word[4:])
print(word[::-1])
print(word[2:50])
print(word[5:2])

9. Exercices — moyens

a) Ce programme est censé afficher Majuscules : TRADING puis Dernière lettre : g, mais aucune des deux lignes ne sort correctement. Trouve les DEUX bugs sans l'exécuter (un silencieux, un qui plante), puis corrige :

word = "trading"
big = word.upper
print(f"Majuscules : {big}")
print(f"Dernière lettre : {word[len(word)]}")

b) Demande le prénom puis le nom (input() × 2, l'utilisateur peut taper en minuscules). Affiche les initiales au format A. L. — première lettre de chaque, en majuscule, suivie d'un point. Contraintes : indexation + upper() ; doit marcher quel que soit ce que tape l'utilisateur (adaA).

c) Un « slug » est un titre transformé en identifiant d'URL. Écris un programme qui transforme title = "Mon Super Article !" en mon-super-article : supprimer le !, enlever les espaces des bouts, tout en minuscules, espaces restants remplacés par des tirets. Tout tient en UNE chaîne d'appels de méthodes — mais attention, l'ordre des maillons change le résultat : trouve un ordre qui marche, et note dans ton journal un ordre qui échoue et pourquoi (indice : que devient l'espace laissé par le ! supprimé ?).

10. Exercices — difficiles

a) Un palindrome se lit dans les deux sens (kayak, Engage le jeu que je le gagne). Demande une phrase à l'utilisateur et affiche True si c'en est un, False sinon — en ignorant majuscules et espaces. ⏩ == (vu en 01-2, section 4a) produit exactement ce True/False ; pas besoin de if.

b) Sans split() : demande une adresse email (ex. ada.lovelace@example.com) et affiche trois lignes — l'utilisateur (ada.lovelace), le domaine (example.com), et l'extension (com). Contraintes : find() + slicing uniquement ; aucune position tapée en dur (ça doit marcher pour bob@x.fr aussi).

11. Mini-projet lié — « nettoyeur de texte »

Écris cleaner.py : l'utilisateur tape une phrase (souvent mal fichue — espaces autour, casse incohérente) et le programme produit une fiche d'analyse alignée.

Comportement attendu (exemple de session) :

=== 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

Contraintes :

Extension (facultative) : affiche aussi Espaces doubles : True/False avec " " in cleaned — et teste ton compteur de mots avec deux espaces entre deux mots. Tu verras sa limite ; note-la dans ton journal (⏩ split() la corrigera au niveau 3).

12. Correction / méthode de correction

Les solutions détaillées sont dans solutions/serie-01-3-solutions.md.

Méthode : (1) finis TOUS les exercices avant d'ouvrir les solutions ; (2) compare le raisonnement, pas seulement le code — une solution différente qui marche est souvent valable ; (3) pour chaque écart, écris une ligne dans ton journal : « je pensais X, en fait Y » ; (4) refais de tête l'exercice raté, le lendemain.

13. À retenir

14. Questions de révision

  1. Que valent word[0], word[-1] et word[len(word)] pour word = "python" ? Lequel des trois pose problème, et pourquoi ?
  2. Pourquoi word[0:3] contient-il exactement 3 caractères ? Quel est l'avantage de cette convention ?
  3. word[2:50] plante-t-il ? Et word[50] ? Explique la différence de comportement entre indexation et slicing.
  4. Que signifie « les chaînes sont immuables » ? Donne le bug bruyant ET le bug silencieux que cette règle provoque.
  5. Pourquoi message.strip() seul sur une ligne ne sert-il à rien ?
  6. Que retourne "abc".find("z") ? Pourquoi est-ce plus dangereux qu'une erreur ?
  7. Quelle est la différence entre len(word) et word.count("a") — dans la syntaxe et dans ce qu'ils mesurent ?
  8. Comment affiches-tu 19.9 sous la forme 19.90 (aligné à droite sur 8, deux décimales) dans une f-string ?

15. Checklist de compréhension

Si une case reste vide : refais les exercices de la section correspondante demain, AVANT d'ouvrir la leçon suivante.

16. Commit conseillé

git add lessons/level-01-bases/exercices/mes-reponses/
git commit -m "exercises: complete lesson 01-3 strings"

Puis, si c'est ta fin de session : entrée de journal + git push.