Working with text: strings
A string is the computer's word for text: a piece of writing made of characters. A character is a single symbol — a letter like A, a digit like 7, a space, a comma, even an emoji. String them together and you get text: a name, a password, a line from a file, a search box, the very sentence you are reading. Because almost every program touches text at some point, knowing how to look inside a string and reshape it is one of the most useful skills you can have. This lesson builds that skill from absolutely nothing.
To tell the computer "this is text, not a command," you wrap it in quotes. Single quotes '...' and double quotes "..." mean exactly the same thing — pick whichever you like, just match the opening one with the same closing one.
name = "Ada" # a string written with double quotes
city = 'Paris' # the same idea with single quotes
The quotes are not part of the text — they are just the fences that mark where the text starts and ends. The string "Ada" contains three characters: A, d, a. A string with nothing between the quotes, "", is the empty string: real, valid text that happens to be zero characters long (think of a blank text box before you type). And if you need text that spans several lines, you can wrap it in triple quotes:
poem = """roses are red
violets are blue""" # a single string holding two lines
:::note The big idea
A string behaves a lot like a read-only list of characters in a fixed order. Each character sits at a numbered position called its index, and — this trips up every single beginner — counting starts at 0, not 1. So in "Ada", position 0 holds the first letter A, position 1 holds d, and position 2 holds the last a. (The "read-only" part matters too; we will return to it.)
:::
To pull out the character at a position, write the string (or its variable) followed by the index in square brackets: name[0]. This is called indexing. Picture the string laid out with its index written under each character:
# A d a
# 0 1 2 <- index of each character
name = "Ada"
print(name[0]) # A (the first character, position 0)
print(name[2]) # a (the third character, position 2)
print(len(name)) # 3 (len = how many characters there are)
len(x) (short for "length") tells you how many characters are in the string. Notice a handy pattern: a string of length 3 has its last character at index 2 — the last valid index is always len - 1, because counting began at 0. Python gives you a shortcut so you do not have to do that subtraction: a negative index counts from the end. name[-1] is the last character, name[-2] the second-to-last, and so on.
name = "Ada"
print(name[-1]) # a (last character, no matter how long)
print(name[-2]) # d (second from the end)
:::note Out of range
What if you ask for a position that does not exist, like name[5] on a 3-letter string? Python stops and reports an IndexError ("string index out of range"). It is refusing to guess what you meant. This is not the computer being broken — it is a guardrail. You will learn to read and fix messages like this in the next lesson, errors & debugging.
:::
Slicing — grabbing a run of characters
Indexing pulls out one character. Slicing pulls out a run of them with s[start:end]. The rule that confuses everyone at first: start is included, but end is excluded. Read the colon as "up to, but not including." So s[1:3] gives you the characters at index 1 and 2 — it stops before 3.
Why exclude the end? Because it makes the math clean: the number of characters you get back is simply end - start. s[1:3] returns 3 - 1 = 2 characters. Picture the indices as fence-posts sitting between the characters:
# c o d e
# 0 1 2 3 4 <- slice cuts happen at these posts
s = "code"
print(s[1:3]) # od (cut at post 1, cut at post 3, take what's between)
You can leave either number blank and Python fills in a sensible default. Leave out start and it means "from the very beginning." Leave out end and it means "all the way to the end."
s = "code"
print(s[:2]) # co (start blank = from 0, up to but not 2)
print(s[2:]) # de (from 2 to the end)
print(s[:]) # code (both blank = a full copy)
A slice can take a third number, the step — how many characters to jump each time: s[start:end:step]. The default step is 1 (every character). A step of 2 takes every other one. And a step of -1 walks backwards, which is the famous one-line way to reverse a string:
s = "code"
print(s[::2]) # cd (every 2nd character: index 0, then 2)
print(s[::-1]) # edoc (step -1 walks end to start = reversed!)
Read s[::-1] piece by piece: start blank, end blank, step -1. Because the step is negative, Python begins at the last character and moves left one at a time until it falls off the front — handing back the whole string flipped. Memorize this shape; it shows up constantly.
Handy string methods
A method is a built-in action you call on a value by attaching a dot and a name, like name.lower(). The empty parentheses () mean "do it now"; some methods take inputs inside those parentheses. Here are the ones you will reach for constantly, each with its output:
.lower()/.upper()— change the letter case."Ada".upper()→"ADA";"Ada".lower()→"ada". Great for comparing text fairly, so "YES" and "yes" count as the same..strip()— trim whitespace (spaces, tabs, line breaks) off both ends, leaving the middle alone." hi ".strip()→"hi". Essential for cleaning up messy input people type..split()— break text into a list of words, cutting on the spaces."a b c".split()→["a", "b", "c"]. (A list, covered earlier, is an ordered collection you can loop over.).join(parts)— the opposite of split: glue a list of strings back into one string, putting the string you called it on between each piece."-".join(["a", "b", "c"])→"a-b-c";" ".join(["hi", "there"])→"hi there"..replace(a, b)— return text with every copy ofaswapped forb."cat".replace("c", "h")→"hat";"a-b-a".replace("a", "x")→"x-b-x"..find(x)/x in s— search inside the text."Ada".find("d")→1(the index where it first appears, or-1if missing). If you only need yes/no,"d" in "Ada"→Trueis cleaner..startswith(x)/.endswith(x)— quick yes/no checks for the front or back."report.pdf".endswith(".pdf")→True.
:::tip The one rule that explains all of this: strings are immutable "Immutable" means unchangeable — once a string exists, you cannot edit a character inside it. There is no "modify in place." So how do the methods above seem to change text? They do not touch the original at all: every one of them returns a brand-new string and leaves the original exactly as it was. :::
This has a practical consequence you must internalize: a method's new string is thrown away unless you save it. Calling name.upper() on its own line does nothing lasting. To keep the result, you must reassign it back to a variable:
name = "ada"
name.upper() # makes "ADA" then discards it!
print(name) # ada (original is untouched)
name = name.upper() # capture the new string back into name
print(name) # ADA (now name points at the new string)
And because a string truly cannot be edited in place, trying to overwrite one character is an outright error — Python will not let you:
name = "ada"
name[0] = "A" # TypeError: 'str' object does not support item assignment
To "change" a character, you build a new string instead — for example with slicing and the + operator (which joins strings end to end): "A" + name[1:] gives "Ada". Same idea, no rule broken: you made a new string rather than mutating the old one.
Building text the clean way: f-strings
To weave variables into text, put an f right before the opening quote (it stands for "formatted"), then drop any variable — or even a small expression — inside {curly braces}. Python evaluates what is in the braces and slots its value into the text. This is called an f-string, and it is the cleanest way to build text.
name = "Ada"
age = 36
print(f"Hi {name}, you are {age}")
# Hi Ada, you are 36
print(f"Next year you'll be {age + 1}") # the braces can hold math too
# Next year you'll be 37
Compare that with the clumsy old way, concatenation — gluing fragments together with +. It forces you to convert numbers with str(...) and to sprinkle in spaces by hand, which is easy to get wrong:
# the awkward + way (notice the manual spaces and str()):
print("Hi " + name + ", you are " + str(age))
# Hi Ada, you are 36 (same result, far harder to read)
Worked example 1 — count the vowels in a word. A string works with a for loop just like a list: the loop hands you one character at a time. We keep a counter starting at 0 and add 1 whenever the current character is a vowel. The test letter in "aeiou" reuses the membership check from before: it asks "is this character one of these five?"
word = "banana"
count = 0
for letter in word:
if letter in "aeiou": # is this letter a vowel?
count = count + 1
print(count) # 3
Let us trace it character by character. Start: count = 0. See b — not a vowel, skip. See a — vowel, count becomes 1. See n — skip. See a — count becomes 2. See n — skip. See a — count becomes 3. The loop ends; we print 3. Small, reusable ideas (looping, the in check, a counter) combine into a real program.
Worked example 2 — reverse a string and check for a palindrome. A palindrome is a word that reads the same forwards and backwards, like "racecar" or "level". We already know s[::-1] flips a string, so checking is one comparison: is the word equal to its own reverse?
def is_palindrome(word):
reversed_word = word[::-1] # build the flipped version
return word == reversed_word # True if they match exactly
print(is_palindrome("racecar")) # True
print(is_palindrome("hello")) # False
Trace "racecar": its reverse is also "racecar", so word == reversed_word is True. Trace "hello": its reverse is "olleh", which is not equal to "hello", so we get False. A real palindrome checker would usually .lower() the text first so capital letters do not throw off the comparison — a nice example of combining a method with a slice.
:::tip Two facts to hold onto Everything here grows from two facts: a string is an ordered sequence of characters you can index and slice, and it is immutable, so methods return new strings rather than changing the old one. Hold onto those two and the rest is just vocabulary. :::
Why it matters
Text is everywhere — names, files, search boxes, user input — so the moves in this lesson come up in nearly every program you'll write. A string is an ordered, zero-indexed sequence of characters you can index (name[0]), slice (s[1:3], and s[::-1] to reverse), and reshape with methods like .lower(), .strip(), .split(), and .replace(). Because strings are immutable, those methods hand you a brand-new string you must save, and f-strings are the clean way to weave values into text. These same indexing, slicing, and membership ideas reappear constantly in text-processing and interview problems.
Where this leads: string basics become real text-processing and interview problems once matching and hashing enter. See Where this leads next.
Practice
Now make the vowel counter your own. Loop over a string's characters, count how many are vowels (a, e, i, o, u — any case), and run it live.
Write count_vowels(s) that returns how many characters of the string s are vowels (a, e, i, o, u), counting both uppercase and lowercase. An empty string has 0 vowels.
Checkpoint
Working with text: strings
Pass to unlock the Next button belowNext: Errors & debugging →