Python — Strings Deep Dive
Strings are one of Python's most versatile data types. They represent sequences of Unicode characters and come with a rich set of methods for manipulation, formatting, and analysis. This guide covers everything from basic creation to advanced formatting and encoding.
Python provides several ways to create strings. Single and double quotes are interchangeable; triple quotes enable multi-line strings.
| 1 | # Single and double quotes (identical behavior) |
| 2 | single = 'Hello' |
| 3 | double = "World" |
| 4 | |
| 5 | # Triple quotes — multi-line strings |
| 6 | multi = """This spans |
| 7 | multiple lines |
| 8 | without escape sequences.""" |
| 9 | |
| 10 | # Triple quotes with single quotes |
| 11 | also_multi = ```Triple single |
| 12 | quotes work too.``` |
| 13 | |
| 14 | # Empty string |
| 15 | empty = "" |
| 16 | empty = str() |
| 17 | |
| 18 | # String from other types |
| 19 | number = str(42) # "42" |
| 20 | float_str = str(3.14) # "3.14" |
| 21 | bool_str = str(True) # "True" |
| 22 | list_str = str([1, 2, 3]) # "[1, 2, 3]" |
| 23 | |
| 24 | # Concatenation at creation |
| 25 | joined = "Hello" " " "World" # "Hello World" (adjacent literals) |
| 26 | |
| 27 | # Repetition |
| 28 | repeated = "Ha" * 3 # "HaHaHa" |
info
Escape sequences let you embed special characters. Raw strings (r"...") disable escaping and are essential for regex patterns and Windows paths.
| Sequence | Meaning |
|---|---|
| \\n | Newline |
| \\t | Horizontal tab |
| \\\\ | Backslash |
| \\' | Single quote |
| \\" | Double quote |
| \\r | Carriage return |
| \\b | Backspace |
| \\f | Form feed |
| \\v | Vertical tab |
| \\0 | Null character |
| \\xhh | Hex value (e.g. \\x41 = A) |
| 1 | # Escape sequences in action |
| 2 | print("Line1\nLine2") # newline |
| 3 | print("Column1\tColumn2") # tab |
| 4 | print("Backslash: \\") # literal backslash |
| 5 | print("Quote: \"Safe\"") # double quote inside double |
| 6 | print('Quote: \'Safe\'') # single quote inside single |
| 7 | |
| 8 | # Raw strings — escape sequences are ignored |
| 9 | path = r"C:\Users\Admin\Documents" |
| 10 | regex = r"\d+\.\d{2}" # matches 3.14, 42.00 etc. |
| 11 | print(path) # prints: C:\\Users\\Admin\\Documents |
| 12 | |
| 13 | # Raw triple-quoted strings |
| 14 | block = r```Multi\nline\traw``` |
| 15 | # Contains literal backslash-n and backslash-t |
f-strings (Python 3.6+) are the recommended string formatting approach. They embed expressions directly inside string literals using {...} syntax.
| 1 | name, age = "Alice", 30 |
| 2 | |
| 3 | # Basic interpolation |
| 4 | greeting = f"Hello, {name}!" |
| 5 | print(greeting) # Hello, Alice! |
| 6 | |
| 7 | # Expressions allowed |
| 8 | print(f"{name} is {age} years old") |
| 9 | print(f"In 5 years, {name} will be {age + 5}") |
| 10 | |
| 11 | # Format specifiers |
| 12 | pi = 3.14159265 |
| 13 | print(f"Pi rounded: {pi:.2f}") # 3.14 |
| 14 | print(f"Pi padded: {pi:10.2f}") # " 3.14" (right-aligned) |
| 15 | print(f"Pi left: {pi:<10.2f}") # "3.14 " (left-aligned) |
| 16 | print(f"Pi center: {pi:^10.2f}") # " 3.14 " (centered) |
| 17 | |
| 18 | # Number formatting |
| 19 | val = 42 |
| 20 | print(f"Binary: {val:b}") # 101010 |
| 21 | print(f"Hex: {val:#x}") # 0x2a |
| 22 | print(f"Octal: {val:#o}") # 0o52 |
| 23 | print(f"Padding: {val:05d}") # 00042 |
| 24 | print(f"Commas: {1000000:,d}") # 1,000,000 |
| 25 | print(f"Percent: {0.125:.1%}") # 12.5% |
| 26 | |
| 27 | # Repr and str conversion |
| 28 | print(f"Repr: {name!r}") # 'Alice' |
| 29 | print(f"Str: {name!s}") # Alice |
| 30 | print(f"ASCII: {name!a}") # 'Alice' |
| 31 | |
| 32 | # Dictionaries and attribute access |
| 33 | data = {"key": "value"} |
| 34 | print(f"Dict: {data['key']}") # value |
| 35 | |
| 36 | class Point: |
| 37 | def __init__(self, x, y): |
| 38 | self.x, self.y = x, y |
| 39 | def __str__(self): |
| 40 | return f"({self.x}, {self.y})" |
| 41 | |
| 42 | p = Point(3, 4) |
| 43 | print(f"Point: {p}") # (3, 4) |
| 44 | |
| 45 | # Multi-line f-strings |
| 46 | msg = ( |
| 47 | f"Name: {name}\n" |
| 48 | f"Age: {age}\n" |
| 49 | f"PI: {pi:.2f}" |
| 50 | ) |
| 51 | print(msg) |
| 52 | |
| 53 | # F-string debugging (3.8+) |
| 54 | x, y = 10, 20 |
| 55 | print(f"{x=}, {y=}") # x=10, y=20 |
| 56 | print(f"{x + y=}") # x + y=30 |
| 57 | print(f"{x=:.2f}") # x=10.00 |
best practice
Before f-strings, Python offered two older formatting approaches. You may encounter them in legacy codebases.
| 1 | # === str.format() (Python 2.6+) === |
| 2 | |
| 3 | # Positional arguments |
| 4 | print("{} is {} years old".format("Alice", 30)) |
| 5 | # "Alice is 30 years old" |
| 6 | |
| 7 | # Indexed positional |
| 8 | print("{1} is {0} years old".format(30, "Alice")) |
| 9 | # "Alice is 30 years old" |
| 10 | |
| 11 | # Named placeholders |
| 12 | print("{name} is {age} years old".format(name="Bob", age=25)) |
| 13 | # "Bob is 25 years old" |
| 14 | |
| 15 | # Format specifiers |
| 16 | print("{:.2f}".format(3.14159)) # "3.14" |
| 17 | print("{:>10}".format("hello")) # " hello" (right) |
| 18 | print("{:<10}".format("hello")) # "hello " (left) |
| 19 | print("{:^10}".format("hello")) # " hello " (center) |
| 20 | print("{:010d}".format(42)) # "0000000042" |
| 21 | print("{:,}".format(1000000)) # "1,000,000" |
| 22 | print("{:.1%}".format(0.25)) # "25.0%" |
| 23 | |
| 24 | # Accessing attributes / items |
| 25 | class Obj: |
| 26 | name = "thing" |
| 27 | print("{0.name}".format(Obj())) # "thing" |
| 28 | print("{0[key]}".format({"key": "val"})) # "val" |
| 29 | |
| 30 | |
| 31 | # === %-formatting (Python < 2.6, C-style) === |
| 32 | |
| 33 | print("%s is %d years old" % ("Alice", 30)) |
| 34 | # "Alice is 30 years old" |
| 35 | |
| 36 | print("PI = %.2f" % 3.14159) # "PI = 3.14" |
| 37 | print("%-10s %05d" % ("left", 42)) # "left 00042" |
| 38 | |
| 39 | # Named mapping |
| 40 | print("%(name)s is %(age)d" % {"name": "Bob", "age": 25}) |
| 41 | # "Bob is 25" |
info
Python's str type provides dozens of built-in methods. Here are the most commonly used ones, grouped by purpose.
Case Transformation
| 1 | text = "hello, World!" |
| 2 | |
| 3 | print(text.upper()) # HELLO, WORLD! |
| 4 | print(text.lower()) # hello, world! |
| 5 | print(text.title()) # Hello, World! |
| 6 | print(text.capitalize()) # Hello, world! |
| 7 | print(text.swapcase()) # HELLO, wORLD! |
| 8 | print(text.casefold()) # hello, world! (aggressive lower for caseless matching) |
Searching & Finding
| 1 | text = "hello world hello" |
| 2 | |
| 3 | print(text.find("world")) # 6 (first index, -1 if not found) |
| 4 | print(text.rfind("hello")) # 12 (last index, -1 if not found) |
| 5 | print(text.index("world")) # 6 (like find but raises ValueError) |
| 6 | print(text.rindex("hello")) # 12 (like rfind but raises ValueError) |
| 7 | print(text.count("hello")) # 2 (non-overlapping occurrences) |
| 8 | print(text.startswith("hello")) # True |
| 9 | print(text.endswith("hello")) # True |
| 10 | print("hello" in text) # True (membership test) |
| 11 | print(text.find("missing")) # -1 |
Trimming & Padding
| 1 | text = " hello world \n" |
| 2 | |
| 3 | print(text.strip()) # "hello world" (removes leading/trailing whitespace) |
| 4 | print(text.lstrip()) # "hello world \n" (left side only) |
| 5 | print(text.rstrip()) # " hello world" (right side only) |
| 6 | |
| 7 | print(text.strip(" hd")) # "ello worl" (strip specific chars) |
| 8 | |
| 9 | # Padding |
| 10 | print("42".zfill(5)) # "00042" (zero-pad left) |
| 11 | print("hi".ljust(6, "-")) # "hi----" |
| 12 | print("hi".rjust(6, "-")) # "----hi" |
| 13 | print("hi".center(6, "-")) # "--hi--" |
Splitting & Joining
| 1 | # Splitting |
| 2 | text = "a,b,c,d" |
| 3 | print(text.split(",")) # ["a", "b", "c", "d"] |
| 4 | print(text.rsplit(",", 2)) # ["a,b", "c", "d"] (right split with max) |
| 5 | print(text.splitlines()) # split on newlines |
| 6 | |
| 7 | # partition — splits into (before, sep, after) |
| 8 | print(text.partition(",")) # ("a", ",", "b,c,d") |
| 9 | print(text.rpartition(",")) # ("a,b,c", ",", "d") |
| 10 | |
| 11 | # Joining — best practice |
| 12 | parts = ["a", "b", "c", "d"] |
| 13 | print(",".join(parts)) # "a,b,c,d" |
| 14 | print(" -> ".join(parts)) # "a -> b -> c -> d" |
| 15 | print("".join(parts)) # "abcd" |
| 16 | |
| 17 | # Joining with generator |
| 18 | print("".join(str(n) for n in range(5))) # "01234" |
Replacement
| 1 | text = "one two one two" |
| 2 | |
| 3 | print(text.replace("one", "1")) # "1 two 1 two" |
| 4 | print(text.replace("one", "1", 1)) # "1 two one two" (max 1 replacement) |
| 5 | print(text.replace(" ", ", ")) # "one, two, one, two" |
| 6 | |
| 7 | # Translate — character mapping via str.maketrans |
| 8 | trans = str.maketrans({"o": "0", "e": "3"}) |
| 9 | print(text.translate(trans)) # "0n3 tw0 0n3 tw0" |
| 10 | |
| 11 | # Remove prefix/suffix (3.9+) |
| 12 | print("hello.py".removeprefix("hello")) # ".py" |
| 13 | print("hello.py".removesuffix(".py")) # "hello" |
Character Classification
| 1 | print("hello".isalpha()) # True (all letters) |
| 2 | print("hello123".isalnum()) # True (letters or digits) |
| 3 | print("123".isdigit()) # True (all digits) |
| 4 | print("\u00B2".isnumeric()) # True (includes superscripts, fractions) |
| 5 | print(" ".isspace()) # True (all whitespace) |
| 6 | print("Hello".istitle()) # True (title-cased) |
| 7 | print("hello".islower()) # True (all lowercase) |
| 8 | print("HELLO".isupper()) # True (all uppercase) |
| 9 | print("Hello".isidentifier()) # True (valid Python identifier) |
| 10 | print("abc".isascii()) # True (all ASCII, Python 3.7+) |
| 11 | print("42".isdecimal()) # True (base-10 digits only) |
| 12 | print("\u2167".isnumeric()) # True (Roman numeral VIII) |
| 13 | print("\u00B2".isdigit()) # True (superscript 2) |
| 14 | print("\u00B2".isdecimal()) # False (superscript not base-10) |
best practice
Slicing extracts substrings using the [start:stop:step] syntax. All indices are zero-based, and stop is exclusive. Negative indices count from the end.
| Slice | Result | Explanation |
|---|---|---|
| s[0:5] | "Hello" | Characters 0 through 4 |
| s[:5] | "Hello" | Start defaults to 0 |
| s[6:] | "World!" | Stop defaults to end |
| s[::2] | "HloWrd" | Every second character |
| s[::-1] | "!dlroW olleH" | Reversed string |
| s[-5:] | "orld!" | Last 5 characters |
| s[-3:-1] | "ld" | Negative indices |
| 1 | s = "Hello, World!" |
| 2 | |
| 3 | # Basic slicing |
| 4 | print(s[0:5]) # "Hello" |
| 5 | print(s[7:12]) # "World" |
| 6 | print(s[:5]) # "Hello" (start = 0) |
| 7 | print(s[7:]) # "World!" (stop = end) |
| 8 | print(s[:]) # "Hello, World!" (full copy) |
| 9 | |
| 10 | # Negative indices |
| 11 | print(s[-6:-1]) # "World" (from -6 up to -1) |
| 12 | print(s[-5:]) # "orld!" (last 5 chars) |
| 13 | |
| 14 | # Step |
| 15 | print(s[::2]) # "HloWrd" (every 2nd char) |
| 16 | print(s[1::2]) # "el,ol!" (every 2nd, starting at 1) |
| 17 | print(s[::-1]) # "!dlroW ,olleH" (reverse) |
| 18 | |
| 19 | # Combined |
| 20 | print(s[2:9:3]) # "l,W" (indices 2,5,8) |
| 21 | print(s[7:2:-1]) # "W ,oll" (reverse slice) |
| 22 | |
| 23 | # Slicing is safe — out-of-range handled gracefully |
| 24 | print(s[0:100]) # "Hello, World!" (no error) |
| 25 | print(s[100:]) # "" (empty string) |
info
Strings are immutable in Python — once created, they cannot be changed in-place. Every operation that modifies a string returns a new string object.
| 1 | s = "hello" |
| 2 | |
| 3 | # This raises TypeError: |
| 4 | # s[0] = "H" # TypeError: 'str' object does not support item assignment |
| 5 | |
| 6 | # Instead, create a new string: |
| 7 | s = "H" + s[1:] # "Hello" |
| 8 | |
| 9 | # Every string method returns a new string |
| 10 | original = " Hello " |
| 11 | stripped = original.strip() # original is unchanged |
| 12 | print(original) # " Hello " (still has spaces) |
| 13 | print(stripped) # "Hello" |
| 14 | |
| 15 | # Concatenation creates new strings (potentially expensive) |
| 16 | s = "" |
| 17 | for word in ["a", "b", "c"]: |
| 18 | s = s + word # new string each iteration — O(n^2) |
| 19 | |
| 20 | # Identity check |
| 21 | a = "hello" |
| 22 | b = "hello" |
| 23 | print(a is b) # True (CPython interning for small strings) |
| 24 | print(id(a) == id(b)) # True (same object) |
| 25 | |
| 26 | # But not all strings are interned |
| 27 | c = "hello world!" |
| 28 | d = "hello world!" |
| 29 | print(c is d) # False (CPython may not intern long strings) |
| 30 | |
| 31 | # Always use == for string comparison, not 'is' |
| 32 | print(c == d) # True (correct) |
Python 3 strings are sequences of Unicode code points. The encode() method converts a string to bytes, while decode() converts bytes back to a string.
| 1 | # Unicode characters by code point |
| 2 | print("\u00F1") # ñ (U+00F1) |
| 3 | print("\U0001F600") # 😀 (U+1F600, emoji) |
| 4 | print("\N{SNOWMAN}") # ☃ (Unicode name) |
| 5 | |
| 6 | # Length — counts code points, not bytes |
| 7 | s = "café" |
| 8 | print(len(s)) # 4 (not 5 — é is one code point) |
| 9 | print(len("😀")) # 1 (emoji is one code point) |
| 10 | |
| 11 | # Encoding: str -> bytes |
| 12 | text = "café" |
| 13 | utf8 = text.encode("utf-8") |
| 14 | utf16 = text.encode("utf-16") |
| 15 | latin = text.encode("latin-1") |
| 16 | |
| 17 | print(utf8) # b'caf\xc3\xa9' |
| 18 | print(len(utf8)) # 5 bytes |
| 19 | print(len(utf16)) # 10 bytes (includes BOM) |
| 20 | |
| 21 | # Decoding: bytes -> str |
| 22 | original = utf8.decode("utf-8") |
| 23 | print(original) # "café" |
| 24 | |
| 25 | # Error handling |
| 26 | try: |
| 27 | latin2 = text.encode("ascii") |
| 28 | except UnicodeEncodeError as e: |
| 29 | print(f"Cannot encode: {e}") # é not in ASCII |
| 30 | |
| 31 | # Ignore/replace for encoding |
| 32 | print(text.encode("ascii", errors="ignore")) # b'caf' |
| 33 | print(text.encode("ascii", errors="replace")) # b'caf?' |
| 34 | print(text.encode("ascii", errors="xmlcharrefreplace")) # b'café' |
| 35 | |
| 36 | # Normalization — important for comparison |
| 37 | from unicodedata import normalize |
| 38 | |
| 39 | s1 = "café" # composed: é is one code point |
| 40 | s2 = "cafe\u0301" # decomposed: e + combining acute accent |
| 41 | |
| 42 | print(s1 == s2) # False (different representations!) |
| 43 | print(len(s1), len(s2)) # 4, 5 |
| 44 | |
| 45 | print(normalize("NFC", s1) == normalize("NFC", s2)) # True (composed) |
| 46 | print(normalize("NFD", s1) == normalize("NFD", s2)) # True (decomposed) |
info
Concatenation: join() vs +
Using + in a loop creates a new string each iteration, leading to O(n²) time. The str.join() method pre-allocates and is always the right choice for joining multiple strings.
| 1 | # BAD: O(n^2) string concatenation in a loop |
| 2 | result = "" |
| 3 | for word in ["this", "is", "slow"]: |
| 4 | result += word + " " |
| 5 | |
| 6 | # GOOD: O(n) join |
| 7 | result = " ".join(["this", "is", "fast"]) |
| 8 | |
| 9 | # Even better: generator expression |
| 10 | result = " ".join(word for word in ["a", "b", "c"]) |
| 11 | |
| 12 | # Pre-allocate list then join |
| 13 | parts = [] |
| 14 | for i in range(100): |
| 15 | parts.append(str(i)) |
| 16 | result = ", ".join(parts) |
| 17 | |
| 18 | # Small number of strings: + is fine |
| 19 | full_name = first_name + " " + last_name |
The string Module
Python's string module provides useful constants and utilities.
| 1 | import string |
| 2 | |
| 3 | # Character constants |
| 4 | print(string.ascii_lowercase) # "abcdefghijklmnopqrstuvwxyz" |
| 5 | print(string.ascii_uppercase) # "ABCDEFGHIJKLMNOPQRSTUVWXYZ" |
| 6 | print(string.ascii_letters) # both lowercase + uppercase |
| 7 | print(string.digits) # "0123456789" |
| 8 | print(string.hexdigits) # "0123456789abcdefABCDEF" |
| 9 | print(string.octdigits) # "01234567" |
| 10 | print(string.punctuation) # "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" |
| 11 | print(string.whitespace) # " \t\n\r\x0b\x0c" |
| 12 | print(string.printable) # all printable characters |
| 13 | |
| 14 | # Practical: generate random password |
| 15 | import random |
| 16 | def random_password(length=12): |
| 17 | chars = string.ascii_letters + string.digits + string.punctuation |
| 18 | return "".join(random.choice(chars) for _ in range(length)) |
| 19 | |
| 20 | print(random_password()) # e.g. "aB3$kL9#xQ2!" |
| 21 | |
| 22 | # Practical: strip punctuation from text |
| 23 | text = "Hello, World!!!" |
| 24 | clean = text.translate(str.maketrans("", "", string.punctuation)) |
| 25 | print(clean) # "Hello World" |
| 26 | |
| 27 | # Practical: check if string contains only letters |
| 28 | print("Hello".strip(string.punctuation)) # "Hello" |
Performance Considerations
| Operation | Complexity | Prefer |
|---|---|---|
| s + t | O(len(s) + len(t)) | Small, fixed concat |
| sep.join(list) | O(total length) | Building from parts |
| s.find(t) | O(n) average | Simple substring search |
| t in s | O(n) average | Membership check |
| s.replace(a, b) | O(n) | Simple replacement |
| s[i:j] | O(j - i) | Extracting substrings |
Quick Reference — Common Patterns
| 1 | # Reverse a string |
| 2 | s[::-1] |
| 3 | |
| 4 | # Check palindrome |
| 5 | s == s[::-1] |
| 6 | |
| 7 | # Remove all whitespace |
| 8 | "".join(s.split()) |
| 9 | |
| 10 | # Count words |
| 11 | len(text.split()) |
| 12 | |
| 13 | # Check if string is uppercase |
| 14 | s.isupper() |
| 15 | |
| 16 | # Convert list to comma-separated string |
| 17 | ", ".join(items) |
| 18 | |
| 19 | # Check if string contains only digits |
| 20 | s.isdigit() |
| 21 | |
| 22 | # Remove prefix/suffix (3.9+) |
| 23 | s.removeprefix("prefix_") |
| 24 | s.removesuffix("_suffix") |
| 25 | |
| 26 | # Repeat character |
| 27 | "-" * 80 |
| 28 | |
| 29 | # First and last character |
| 30 | s[0], s[-1] |
| 31 | |
| 32 | # String padding |
| 33 | f"{val:>10}" |
| 34 | f"{val:<10}" |
| 35 | f"{val:^10}" |
| 36 | |
| 37 | # Check if string starts/ends with multiple options |
| 38 | s.startswith(("http://", "https://")) |
| 39 | s.endswith((".jpg", ".png", ".gif")) |
| 40 | |
| 41 | # Split once |
| 42 | key, sep, value = s.partition("=") |
| 43 | |
| 44 | # Remove duplicate spaces |
| 45 | " ".join(s.split()) |
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.