Strings are how Python programs hold any kind of text: a customer's name, a product title, an email address, an order ID. The type is str, and every product page, search box, and confirmation message you've ever clicked is, somewhere, a str value moving through code. This lesson covers what a string is, how to write one, how to count its characters, how to glue strings together, how to peek inside one character at a time, and how strings behave in if and for.
A string is a sequence of characters. Concretely, it's a value of the built-in type str, and you create one by wrapping text in quotes:
The variable doesn't store the characters one by one in some loose pile. It stores a single str object that holds the whole sequence in order. The order matters: "abc" and "cba" are different strings, even though they share the same characters.
Python treats a string as a sequence in the same way it treats a list. You can ask for its length, check whether something is in it, grab one character by position, and walk through it with a for loop. We'll go through each of those in turn.
Two more things to know up front. First, a string can be empty. "" is a valid str with zero characters in it, and you'll create plenty of empty strings as starting points for accumulators or as default values. Second, strings are immutable: once you create one, you can't change a character inside it.
Python lets you write a string with single quotes or double quotes. Both produce the exact same str object:
There's no "double-quote string" type and "single-quote string" type. The quote characters are just the boundary markers Python uses to figure out where the string starts and ends.
So why two choices? Because text often contains a quote character, and you want the freedom to pick the boundary that keeps the value readable. If your string has an apostrophe in it, wrap it in double quotes so the apostrophe doesn't end the string early:
If your string has double quotes inside it, wrap it in single quotes:
Pick the quote style that lets the value sit cleanly inside without escaping. When the text contains neither, the choice is style. Many Python codebases default to double quotes (the black formatter, for example, normalizes everything to double quotes), but plenty of projects use single quotes everywhere. Both are fine; just be consistent within a project.
If a string contains both single and double quotes, you have a few options. The simplest is to escape one of them with a backslash:
The \" tells Python "this double quote is part of the string, not the end of it." For now, just know the trick exists.
len()To find out how many characters a string has, pass it to the built-in len():
len() counts every character, including spaces and punctuation. The space between "Wireless" and "Headphones" adds one to the count. The @ and . in the email each count as one character. An empty string has length 0.
len() is a built-in function, not a method on the string. You call it as len(s), not s.len(). This is the same len() you've already used (or will use) on lists and other sequences.
A common use case is checking whether some text the customer typed is short enough or long enough to be valid:
If the customer typed "hi", the length would be 2 and the first branch would fire. Length checks like this are everywhere in form validation: a name must be at least one character, a postal code must be exactly six, an email can't be longer than 320 characters.
One subtle point. len() counts characters, not bytes. For plain ASCII text the two happen to match, but for text that includes accented letters or characters from other writing systems they won't. For now, treat len() as "number of characters."
str()You'll often have a value that isn't text yet (a number, a boolean) that you want to treat as a string, usually so you can stick it inside a message. The built-in str() does that conversion:
The original quantity is still an integer. str(quantity) returns a brand-new str object that contains the characters '3'. Same idea for the float and the boolean. The original variables are untouched, you just have a string version available now.
Why bother converting? Because Python is strict about mixing types in some places. The next section shows where trying to glue a number directly onto a string fails until you convert it.
str() is the no-frills version: take a value, give me its default text form. There's a lot more to formatting numbers (currency symbols, fixed decimal places, padding) for which Python provides richer tools.
+The + operator joins two strings end-to-end. The result is a new string containing the characters from the left side followed by the characters from the right side:
Three strings glued together: the first name, a single space, and the last name. Notice that the space had to be its own string. + doesn't add spaces for you. If you write first_name + last_name, you get "AaravMehta", no gap.
You can build up longer messages the same way:
This is the most basic way to assemble text from pieces. It's not always the prettiest (the chain of + and quoted spaces gets noisy fast), and Python has cleaner alternatives like f-strings. For two or three pieces, + is fine.
The big rule: both operands must be strings. Python won't quietly convert a number into text for you. This breaks:
What's wrong with this code?
quantity is an int, not a str. The + operator doesn't know how to combine those two types, so it raises TypeError. The fix is to convert the integer to a string first with str():
Fix:
str(quantity) produces "3", which + is happy to glue onto the surrounding text.
A different (and often cleaner) workaround is to let print itself handle the mixed types. print() accepts any number of arguments of any type, converts each one to its string form, and joins them with a space:
No str() needed, no + needed. The trade-off: you can't capture this into a variable the same way. print() returns None, and the string it builds lives only on the screen. When you need the assembled string for further processing, you'll convert and concatenate. When you just want to display it, print() with multiple arguments is the easy path.
Cost: Building a long string by repeated += inside a loop is slower than it looks. Each += allocates a fresh string and copies every character from both sides. For two or three pieces it's invisible; for thousands of pieces it becomes a real cost. The standard fix is "".join(parts).
*Multiplying a string by an integer repeats it that many times. The result is a new string:
"-" * 30 produces a string of thirty hyphens. "= " * 10 repeats the two-character pattern ten times, giving a 20-character string with alternating equals signs and spaces. Repetition is handy for printing simple visual dividers between sections of a receipt or report.
Order doesn't matter: "-" * 30 and 30 * "-" give the same result. Both forms are valid Python.
If you multiply by zero or a negative number, you get an empty string:
repr() is shown here so the empty strings are visible (otherwise print('') just prints a blank line). Both produce a str of length 0. Python doesn't raise an error for negative counts; it treats them as zero. That's worth knowing: if a quantity slips into the wrong sign somewhere upstream, you'll get an empty string rather than a crash.
The other operand must be an integer. "abc" * 2.5 raises TypeError. You can't repeat a string a fractional number of times.
A nice combination of + and * for visual receipts:
Two rows of equals signs sandwiching a centered title. The title gets centered by prepending the right number of spaces. Crude, but it works for quick console output. The trick of "=" * 40 for a horizontal rule is something you'll keep using.
in and not inTo check whether a string contains a smaller string, use in. The result is a boolean:
"Bluetooth" in product_name is True because those exact characters appear somewhere inside the larger string. "USB" in product_name is False. "phones" in product_name is also True, because "phones" is the tail end of "Headphones". The match doesn't have to align with word boundaries.
in is case-sensitive: "bluetooth" in product_name would be False because the stored value capitalizes the B. Case-insensitive checks come up once you know string methods.
in works against single characters too, since a single character is just a string of length one:
A common pattern is a quick category filter for a search box. Here in does the substring check directly, with no methods needed:
Two of the four titles contain the substring "Headphones", so they get printed. This is the simplest possible search: substring match, exact case.
not in is just the inverse. It's clearer than writing not (x in y):
Use not in when the natural way to read the condition is "X isn't in Y."
A string is a sequence, and like any sequence in Python, you can grab one character at a time by writing the position in square brackets. Position counts from zero on the left:
product[0] is the first character, product[1] is the second, and so on. The last character of "laptop" sits at index 5, because the string has six characters and indexing starts at zero.
You can also count from the right using negative indices. -1 is the last character, -2 is the second-to-last, and so on:
product[-6] and product[0] refer to the same character (the leading l), just from opposite directions. Negative indices are useful when you don't know (or don't want to compute) the length of the string.
Here's the index map for "laptop":
Each box shows the character along with both its forward and backward index. Forward goes left-to-right starting at 0. Backward goes right-to-left starting at -1. Both index systems point to the exact same characters; you choose whichever reads more clearly for what you're doing.
The result of indexing a string is always another string, of length one. There's no separate "char" type in Python:
first is a str of length 1. This is different from many other languages, which have a distinct character type. In Python, a single character is just a one-character string, and it behaves like every other string.
If you ask for an index that isn't there, you get an IndexError:
"laptop" only has six characters, so the largest valid forward index is 5 and the smallest valid negative index is -6. Anything outside that range raises IndexError immediately. There's no "default" or "empty" character returned for an out-of-range index. If you're not sure a string is long enough, check len() first:
This is the standard guard pattern: check the length, then index. Indexing a single character at a time is fine for occasional access. When you want a slice (the first three characters, the last four characters, every other character), Python has a much nicer notation called slicing.
forBecause a string is a sequence, you can walk through its characters one at a time with a for loop. The loop variable holds each character in turn:
Six iterations, one per character, in left-to-right order. There's no need to manage an index by hand. Python takes care of stepping through the sequence and binds each character to the loop variable.
The loop variable can be named anything; char is just a common convention. Some people use c, others use letter. Pick what reads well:
This loop scans every character, and every time it sees an @, it bumps a counter. The same idea works for counting digits, spaces, or any other character class.
A useful pattern uses the boolean-as-integer trick, combined with sum():
The generator expression yields True and False, sum() adds them as 1s and 0s, and you get the count without writing an explicit counter and if. Same outcome as the loop above, fewer lines.
If you also want the index alongside the character, use enumerate():
enumerate() pairs each character with its position. Without it, you'd be tempted to write a range(len(product)) loop and index into the string by hand, which works but reads worse.
One thing to keep in mind: iteration always goes from the start of the string to the end, one character at a time. If you want every other character, the last three characters, or the string in reverse, that's slicing. For "look at every character once," for char in s is the right tool.
"" is a perfectly valid string, just one with zero characters. You'll create empty strings as starting points for accumulators, as default values when you have nothing to show, and as the result of operations that simply produce no characters.
repr() shows the empty string with its surrounding quotes so it's actually visible. Without repr(), print(greeting) would print a blank line and you couldn't tell the empty string apart from a regular newline.
In Python, every value has a truth value. When a value is used in an if condition or a while loop or with bool(), Python decides whether to treat it as true or false. For strings, the rule is simple: an empty string is falsy, every other string (no matter what's in it) is truthy:
The first one is False because the string has no characters. Every other case is True, including " " (a single space, one character), "0" (one character, the digit zero), and "False" (the letters spelling out False). Python doesn't look at what the string says, only at whether it has any characters at all.
This makes empty-checks read naturally:
Writing if customer_address: is the Pythonic way to ask "does this customer have an address?" You don't need if customer_address != "": or if len(customer_address) > 0:, although both would also work. The shorter form is preferred in idiomatic Python.
The same trick works for not:
not search_query is True exactly when search_query is empty. Reads cleanly: "if there's no search query, ask for one."
One pitfall to be aware of: a string containing only whitespace (like " ") is not empty. It has three space characters in it, so it's truthy:
If you want to treat a whitespace-only string as "empty for practical purposes," you'll need to strip the whitespace first using a string method like strip(). For now, just remember that "empty" means zero characters, and a string of spaces isn't empty.
Once a string exists, you can't change a character inside it. Trying to assign into a position raises an error:
To "change" a string, you build a new string and re-bind the name:
The original "laptop" object isn't modified; it's discarded (assuming nothing else points at it) and product now refers to the brand-new string "Laptop". The takeaway here is just the rule: a str object is fixed once created.
10 quizzes