BASIC Python (Beginner Level) 📂 Basic Python Programming · 7 of 15 54 min read

Python Strings — Immutability, Indexing, Slicing, Methods & f-Strings Explained

Master Python strings from the ground up. This tutorial explains the four ways to create strings, the exact meaning of immutability and the four reasons Python enforces it, zero-based and negative indexing, complete slicing syntax with start/stop/step, 50+ built-in string methods with categorised examples (case, cleaning, search, split/join, checking, padding), modern f-string formatting with format specifiers, escape sequences, and a full practice program.

Section 01

The Story That Explains Strings

Beads on a String
Imagine a jewellery maker holding a string of beads. Each bead has a letter on it — H, E, L, L, O. Together they spell "HELLO". You can look at any bead by counting from the left (bead 0 is 'H', bead 4 is 'O'). You can take a section of beads (beads 1 to 3 spells "ELL"). You can make a new string with the same beads plus more (add "!" to get "HELLO!").

But here is the catch: this is a museum piece. You are not allowed to change a bead once it's on the string. If you want to change 'H' to 'J' and get "JELLO", you must make a brand-new string from scratch. The original "HELLO" stays untouched forever.

That is exactly how Python strings work. They are sequences of characters that you can inspect, slice, and combine — but never modify in place. This property is called immutability, and it is one of the most important design decisions in Python.

A string (str) is a sequence of characters enclosed in quotes. Strings represent all text data in Python — names, messages, filenames, URLs, JSON, HTML, everything. Strings are the data type you will use most often in real programs.

💡
Two Non-Negotiable Facts About Strings

1. A string is a sequence — an ordered collection of characters, each at a numbered position (index). This is why indexing and slicing work.
2. A string is immutable — once created, its contents cannot change. Every "modification" produces a brand-new string.


Section 02

Creating Strings — Four Ways

Python offers four distinct ways to create a string. Each has a specific purpose — knowing when to use which is the first mark of a fluent Python developer.

'
Single Quotes
'text'
Standard string literals. Use when your text contains double quotes naturally: 'She said "hi"'.
"
Double Quotes
"text"
Identical to single quotes. Preferred when your text contains apostrophes: "it's fine".
Triple Quotes
"""text"""
Multi-line strings. Preserves newlines and indentation exactly. Also used for docstrings inside functions and classes.
🔁
Raw Strings
r"text"
Treats backslashes literally. Perfect for Windows paths and regular expressions: r"C:\Users\Alice".
# ─── FOUR WAYS TO CREATE STRINGS ────────────────────────

# 1. Single quotes
name = 'Alice'
quote = 'She said "Python is fun"'       # double quotes inside — no escaping

# 2. Double quotes (identical behaviour)
message = "Hello, World!"
apostrophe = "it's a wonderful day"      # apostrophe inside — no escaping

# 3. Triple quotes — multi-line, preserves formatting
poem = """Roses are red,
Violets are blue,
Python is awesome,
And so are you."""

# Used for docstrings too
def greet(name):
    """Return a greeting for the given name."""
    return f"Hello, {name}!"

# 4. Raw strings — backslashes are literal
windows_path = r"C:\Users\Alice\new_folder"
regex_pattern = r"\d+\.\d+"              # matches decimal numbers

print(name, message)
print(poem)
print(windows_path)
OUTPUT
Alice Hello, World! Roses are red, Violets are blue, Python is awesome, And so are you. C:\Users\Alice\new_folder
⚠️
Empty Strings Are Real Strings

"" and '' are both perfectly valid strings — just with zero characters. They are falsy in boolean contexts, which is why if my_string: is the Pythonic way to check for "not empty".


Section 03

Immutability — What It Really Means

A Printed Book
Think of a printed hardcover book. Once it comes off the press, you cannot change the words inside. You can read any page. You can photocopy pages. You can write a completely new book with edits. But you cannot walk up to page 47 and change "The cat sat" to "The dog sat" — the physical ink is fixed.

Python strings are printed books. Once created, the sequence of characters inside the string never changes. Any operation that appears to "modify" a string is actually creating a brand-new string and leaving the original untouched.

This is the exact meaning of immutability: the internal state of the object cannot change after it is created.

Immutability in Python has a precise, technical meaning: the object's data cannot be modified after creation. You can still reassign the variable name to a different string — that just points the label at a new object. The original string remains unchanged and is eventually garbage-collected.

Immutability in Action — See It for Yourself

# ─── ATTEMPT TO MODIFY A STRING (WILL FAIL) ─────────────
word = "Python"

# Try to change the first character:
# word[0] = "J"  → TypeError: 'str' object does not support item assignment

# ─── PROOF THAT STRING METHODS CREATE NEW STRINGS ───────
original = "hello"
uppercase = original.upper()

print(original)         # hello     — original unchanged!
print(uppercase)        # HELLO     — brand new string
print(id(original))     # e.g. 140234563  (memory address)
print(id(uppercase))    # e.g. 140234789  (different address!)

# ─── REASSIGNMENT IS NOT MUTATION ───────────────────────
name = "Alice"
print(id(name))          # e.g. 140000

name = name + " Smith"    # looks like modification, but…
print(id(name))          # e.g. 140099  ← different object!

# The original "Alice" still existed briefly — just no name pointed at it.
# Python's garbage collector cleaned it up.
OUTPUT
hello HELLO 140234563 140234789 140000 140099

Mutable vs Immutable — Side by Side

✅ Immutable — str, int, float, tuple, bool
PropertyBehaviour
Change in placeImpossible
ModificationCreates a new object
Memory addressChanges after each op
Can be dict key?Yes
Thread-safeYes
Example ops + "!" → new string
❌ Mutable — list, dict, set
PropertyBehaviour
Change in placeYes, allowed
ModificationSame object, new content
Memory addressSame after ops
Can be dict key?No
Thread-safeNo — needs locks
Example oplst.append(1) — no new list

Section 04

Why Immutability Exists — The Design Rationale

This is not an arbitrary decision or an oversight — Python's designers made strings immutable on purpose. There are four powerful reasons behind it. Understanding these reasons makes you a better programmer and clarifies when to use strings vs lists.

Performance & Hashability
Because strings never change, Python can compute their hash once and cache it forever. This makes strings usable as dictionary keys and set members — one of Python's most important optimisations. Mutable objects like lists cannot be hashed.
Hash = compute-once
🔒
Thread Safety
Multiple threads can read the same string simultaneously with zero risk of corruption. No locks, no mutexes, no race conditions. If nothing can change, nothing can go wrong. This makes concurrent programs vastly simpler.
No race conditions
🛠️
Memory Optimisation (Interning)
Python caches short strings and reuses the same object for identical literals. Two variables holding "hello" often point to the exact same memory address. This saves memory across large programs.
Same content = same object
📈
Predictable Behaviour
When you pass a string to a function, you know it cannot be modified by that function. This eliminates entire classes of bugs where "spooky action at a distance" changes your data unexpectedly. Immutability makes code reasoning easier.
No hidden side effects

Proof: String Interning in Action

# ─── PYTHON REUSES SHORT STRINGS AUTOMATICALLY ──────────
a = "hello"
b = "hello"

print(a is b)              # True — same object in memory!
print(id(a) == id(b))       # True — same address

# ─── HASHABILITY: STRINGS AS DICT KEYS ──────────────────
user_ages = {
    "alice": 30,       # string keys — works!
    "bob": 25,
    "carol": 35
}
print(user_ages["alice"])   # 30 — O(1) lookup thanks to hashing

# TRY THIS WITH A LIST (fails):
# bad = {[1,2,3]: "value"}  → TypeError: unhashable type: 'list'
🎯
The Trade-Off Worth Making

Immutability costs a tiny amount of memory (creating a new string for each modification) but delivers huge wins in performance, safety, and predictability. For text data — which is inspected far more often than modified — the trade-off is overwhelmingly worth it. This is why every major language (Java, C#, Swift, JavaScript) also makes strings immutable.


Section 05

String Indexing — Accessing Individual Characters

Because a string is a sequence, every character has a position number (index). Python uses zero-based indexing — the first character is at index 0, not 1. You can also use negative indices to count backwards from the end.

📏 String Indexing Visualised — "PYTHON"
Character
P    Y    T    H    O    N
Positive
0    1    2    3    4    5 — counts from left, starts at 0
Negative
-6  -5  -4  -3  -2  -1 — counts from right, starts at -1
Length
len("PYTHON") = 6 — total number of characters
# ─── POSITIVE INDEXING (left to right, from 0) ──────────
word = "PYTHON"

print(word[0])             # P   first character
print(word[1])             # Y   second character
print(word[5])             # N   sixth character (last)

# ─── NEGATIVE INDEXING (right to left, from -1) ─────────
print(word[-1])            # N   last character
print(word[-2])            # O   second to last
print(word[-6])            # P   first character (also)

# ─── LENGTH FUNCTION ────────────────────────────────────
print(len(word))          # 6

# ─── OUT-OF-RANGE INDEXING CRASHES ──────────────────────
# word[10]  → IndexError: string index out of range
# word[-10] → IndexError: string index out of range

# ─── PRACTICAL: LAST CHARACTER WITHOUT LEN() ────────────
name = "Alexander"
last_letter = name[-1]        # r — shorter than name[len(name)-1]
first_letter = name[0]        # A
print(f"{name} starts with '{first_letter}' and ends with '{last_letter}'")
OUTPUT
P Y N N O P 6 Alexander starts with 'A' and ends with 'r'
🔑
Negative Indexing Is a Superpower

In C or Java, getting the last character of a string requires word[word.length - 1] — ugly and error-prone. In Python, word[-1] just works. Use negative indices liberally — they make code cleaner and match how humans naturally think ("the second to last item").


Section 06

String Slicing — Grabbing Substrings

Slicing extracts a portion of a string. The syntax is string[start:stop:step]. All three parts are optional, giving you incredible flexibility with minimal code.

Slice Syntax
s[start : stop : step]
start = index to begin (inclusive) — default 0
stop = index to end (exclusive!) — default end
step = how many chars to skip — default 1
Golden Rule
stop is EXCLUSIVE
s[2:5] gives you indices 2, 3, 4not 5. This trips up every beginner exactly once.
# ─── BASIC SLICING ──────────────────────────────────────
s = "Python Programming"
#    0123456789...

print(s[0:6])            # "Python"        indices 0,1,2,3,4,5 (NOT 6!)
print(s[7:18])           # "Programming"   indices 7 through 17

# ─── OMITTED START — DEFAULTS TO 0 ──────────────────────
print(s[:6])             # "Python"

# ─── OMITTED STOP — DEFAULTS TO END ─────────────────────
print(s[7:])             # "Programming"

# ─── OMITTED BOTH — COPIES THE STRING ───────────────────
print(s[:])              # "Python Programming"

# ─── NEGATIVE INDICES IN SLICES ─────────────────────────
print(s[-11:])          # "Programming"   last 11 characters
print(s[:-12])          # "Python"        everything except last 12

# ─── STEP: EVERY Nth CHARACTER ──────────────────────────
print(s[::2])            # "Pto rgamn"    every 2nd character
print(s[0:6:2])          # "Pto"          every 2nd, from 0 to 5

# ─── REVERSE A STRING (classic trick) ───────────────────
print(s[::-1])           # "gnimmargorP nohtyP"   step = -1 reverses

# ─── SAFE: SLICING NEVER CRASHES ────────────────────────
print(s[100:200])        # ""  — returns empty string, no error!
OUTPUT
Python Programming Python Programming Python Programming Programming Python Pto rgamn Pto gnimmargorP nohtyP

Slicing Cheat Sheet

PatternMeaningExample on "PYTHON"
s[a:b]Characters from index a to b-1s[1:4]"YTH"
s[:n]First n characterss[:3]"PYT"
s[n:]Everything from index n to ends[3:]"HON"
s[-n:]Last n characterss[-3:]"HON"
s[:-n]Everything except last n characterss[:-3]"PYT"
s[::-1]Reverse the entire strings[::-1]"NOHTYP"
s[::2]Every 2nd characters[::2]"PTO"
s[:]Copy of the entire strings[:]"PYTHON"

Section 07

String Methods — The Essential Toolkit

Python strings come with 50+ built-in methods. You don't need to memorise them all — but the following categories cover 95% of real-world string work. Remember: every method returns a new string (the original is never modified).

Case Methods

# ─── CHANGING CASE ──────────────────────────────────────
text = "hello WORLD from Python"

print(text.upper())           # HELLO WORLD FROM PYTHON
print(text.lower())           # hello world from python
print(text.title())           # Hello World From Python
print(text.capitalize())      # Hello world from python
print(text.swapcase())        # HELLO world FROM pYTHON

# The original is unchanged
print(text)                   # hello WORLD from Python (unchanged!)

Whitespace & Cleaning Methods

# ─── STRIPPING WHITESPACE ───────────────────────────────
messy = "   Hello, World!   \n"

print(repr(messy.strip()))     # 'Hello, World!'  (both ends)
print(repr(messy.lstrip()))    # 'Hello, World!   \n'  (left only)
print(repr(messy.rstrip()))    # '   Hello, World!'  (right only)

# Strip specific characters (not just whitespace)
url = "https://example.com/"
print(url.strip("/"))          # https://example.com
print(url.rstrip("/"))         # https://example.com

Search & Replace Methods

# ─── FINDING SUBSTRINGS ─────────────────────────────────
sentence = "Python is great, Python is easy"

print(sentence.find("Python"))       # 0    — index of first match
print(sentence.find("Java"))         # -1   — not found (safe)
print(sentence.rfind("Python"))      # 17   — index of last match

# index() is like find() but raises ValueError if not found
print(sentence.index("is"))          # 7
# sentence.index("Java")  → ValueError!

# count occurrences
print(sentence.count("Python"))      # 2
print(sentence.count(" "))           # 5 (spaces)

# ─── REPLACING SUBSTRINGS ───────────────────────────────
print(sentence.replace("Python", "Ruby"))
# Ruby is great, Ruby is easy

# Replace only the first N occurrences
print(sentence.replace("Python", "Ruby", 1))
# Ruby is great, Python is easy

Split & Join Methods — The Power Couple

# ─── SPLIT — string → list ──────────────────────────────
csv = "apple,banana,cherry,date"
fruits = csv.split(",")
print(fruits)                # ['apple', 'banana', 'cherry', 'date']

# Split by whitespace (any amount)
sentence = "  hello   world  from    python  "
words = sentence.split()        # no argument = split on whitespace
print(words)                 # ['hello', 'world', 'from', 'python']

# Split with a limit
print(csv.split(",", 2))       # ['apple', 'banana', 'cherry,date']

# Split by lines
poem = "line 1\nline 2\nline 3"
print(poem.splitlines())        # ['line 1', 'line 2', 'line 3']

rsplit — Splitting from the Right

rsplit() is the mirror image of split(). It splits the string from the right side instead of the left. Without a maxsplit limit, both methods produce identical results — the difference only shows up when you use the maxsplit argument, which controls how many splits to perform.

✅ split(sep, maxsplit)
BehaviourResult
Splits fromLeft → Right
First N splitsLeft side
Leftover ends upOn the right
Use whenYou care about the start
✅ rsplit(sep, maxsplit)
BehaviourResult
Splits fromRight → Left
First N splitsRight side
Leftover ends upOn the left
Use whenYou care about the end
# ─── SPLIT VS RSPLIT — NO MAXSPLIT ──────────────────────
# Without a limit, both give the same result
csv = "apple,banana,cherry,date"

print(csv.split(","))       # ['apple', 'banana', 'cherry', 'date']
print(csv.rsplit(","))      # ['apple', 'banana', 'cherry', 'date']  (identical)

# ─── THE DIFFERENCE APPEARS WITH MAXSPLIT ───────────────
# split — leftover stays on the RIGHT
print(csv.split(",", 1))
# ['apple', 'banana,cherry,date']

# rsplit — leftover stays on the LEFT
print(csv.rsplit(",", 1))
# ['apple,banana,cherry', 'date']

# With maxsplit=2
print(csv.split(",", 2))
# ['apple', 'banana', 'cherry,date']

print(csv.rsplit(",", 2))
# ['apple,banana', 'cherry', 'date']

# ─── REAL-WORLD USE 1: FILENAME & EXTENSION ─────────────
# A filename can contain dots — only the LAST one separates the extension
filename = "my.backup.v2.tar.gz"

# split fails — grabs the FIRST dot
print(filename.split(".", 1))
# ['my', 'backup.v2.tar.gz']    ← wrong split!

# rsplit gets it right — grabs the LAST dot
name, ext = filename.rsplit(".", 1)
print(f"Name: {name}, Extension: {ext}")
# Name: my.backup.v2.tar, Extension: gz

# ─── REAL-WORLD USE 2: FULL NAME PARSING ────────────────
# Assume last word is surname, rest is first/middle names
full_name = "Mary Jane Watson Parker"

first_names, surname = full_name.rsplit(" ", 1)
print(f"First names: {first_names}")   # Mary Jane Watson
print(f"Surname:     {surname}")       # Parker

# ─── REAL-WORLD USE 3: FILE PATH — GET FILENAME ─────────
path = "/home/alice/projects/python/hello.py"

folder, file = path.rsplit("/", 1)
print(f"Folder: {folder}")              # /home/alice/projects/python
print(f"File:   {file}")                # hello.py

# ─── REAL-WORLD USE 4: URL — GET LAST SEGMENT ───────────
url = "https://blog.example.com/2024/python/strings-tutorial"

base, slug = url.rsplit("/", 1)
print(f"Slug: {slug}")                  # strings-tutorial
OUTPUT
['apple', 'banana', 'cherry', 'date'] ['apple', 'banana', 'cherry', 'date'] ['apple', 'banana,cherry,date'] ['apple,banana,cherry', 'date'] ['apple', 'banana', 'cherry,date'] ['apple,banana', 'cherry', 'date'] ['my', 'backup.v2.tar.gz'] Name: my.backup.v2.tar, Extension: gz First names: Mary Jane Watson Surname: Parker Folder: /home/alice/projects/python File: hello.py Slug: strings-tutorial
🔑
When to Reach for rsplit

Use rsplit(sep, 1) whenever the last occurrence of a separator matters — file extensions, surnames, URL slugs, log timestamps, module paths ("a.b.c.func".rsplit(".", 1)["a.b.c", "func"]). Whenever you catch yourself writing s.split(x)[-1] or using a negative index to grab the tail, rsplit(x, 1) is usually the cleaner intent — and slightly faster because Python stops as soon as it has what it needs.

# ─── JOIN — list → string ───────────────────────────────
# "separator".join(iterable)
words = ["Python", "is", "awesome"]

print(" ".join(words))       # Python is awesome
print("-".join(words))       # Python-is-awesome
print(", ".join(words))      # Python, is, awesome
print("".join(words))        # Pythonisawesome

# ─── SPLIT + JOIN PATTERN (common) ──────────────────────
# Standardise a messy string:
messy = "  hello   world  from    python  "
clean = " ".join(messy.split())    # split on whitespace, rejoin with single space
print(repr(clean))              # 'hello world from python'

Checking Methods (all return bool)

# ─── STARTS/ENDS ────────────────────────────────────────
filename = "report_2024.pdf"

print(filename.startswith("report"))    # True
print(filename.endswith(".pdf"))        # True
print(filename.endswith((".pdf", ".doc")))  # True (tuple of options)

# ─── CONTENT CHECKS ─────────────────────────────────────
print("12345".isdigit())       # True — all digits
print("12.34".isdigit())       # False — the dot isn't a digit
print("hello".isalpha())       # True — all letters
print("hello123".isalnum())    # True — letters and digits
print("   ".isspace())         # True — only whitespace
print("Hello".isupper())       # False
print("HELLO".isupper())       # True
print("hello".islower())       # True

Padding & Alignment

# ─── PADDING FOR FIXED-WIDTH OUTPUT ─────────────────────
name = "Alice"

print(name.ljust(15, "."))    # Alice..........
print(name.rjust(15, "."))    # ..........Alice
print(name.center(15, "-"))   # -----Alice-----

# zfill — pad numbers with leading zeros
print("42".zfill(5))          # 00042
print("-42".zfill(5))         # -0042 (sign kept at front)

Complete Methods Reference

CategoryMethodWhat It Does
Case.upper()ALL UPPERCASE
.lower()all lowercase
.title()Title Case Each Word
.capitalize()First letter capital only
.swapcase()Invert all cases
Cleaning.strip()Remove whitespace from both ends
.lstrip() / .rstrip()Strip only left / right
.replace(old, new)Replace all occurrences
Search.find(sub)Index of first match, -1 if not found
.index(sub)Same as find, but raises error if not found
.count(sub)How many times substring appears
"x" in sBoolean membership check
Split/Join.split(sep, n)String → list; first n splits from the left
.rsplit(sep, n)String → list; first n splits from the right
.splitlines()Split by newlines
sep.join(list)List → string with separator
Checks.startswith(pre)Does string start with pre?
.endswith(suf)Does string end with suf?
.isdigit() / .isalpha()All digits / all letters?
.isupper() / .islower()All uppercase / all lowercase?
Padding.ljust(n) / .rjust(n)Left / right pad to width n
.center(n)Center within width n
.zfill(n)Zero-pad numbers

Section 08

Concatenation, Repetition & Membership

# ─── CONCATENATION WITH + ───────────────────────────────
first = "Hello"
last  = "World"

greeting = first + ", " + last + "!"
print(greeting)               # Hello, World!

# You cannot concatenate a string with a number directly
age = 25
# msg = "I am " + age        → TypeError: can only concatenate str to str
msg = "I am " + str(age)       # must convert first
# Or better: use an f-string (see next section!)

# ─── REPETITION WITH * ──────────────────────────────────
line = "-" * 40                 # 40-character horizontal rule
print(line)

print("HA" * 3)                 # HAHAHA
print("> " * 5)                 # > > > > >

# ─── MEMBERSHIP WITH in / not in ────────────────────────
sentence = "The quick brown fox jumps over the lazy dog"

print("fox" in sentence)           # True — case-sensitive substring check
print("cat" not in sentence)       # True
print("FOX" in sentence)           # False — capital F not present

# Case-insensitive check
print("FOX".lower() in sentence.lower())   # True

Section 09

f-Strings — Modern String Formatting

Mail-Merge for Strings
Think of an email newsletter that opens with "Dear {first_name}". The template stays the same — only the placeholder changes for each recipient. Mail-merge fills in the blanks and produces a personalised message.

f-strings (formatted string literals) are Python's mail-merge. You write curly braces {} containing any Python expression, prefix the string with the letter f, and Python fills in the values at runtime. They are fast, readable, and powerful. Introduced in Python 3.6, they've replaced every older formatting method.

Basic f-Strings

# ─── SIMPLE VARIABLE INSERTION ──────────────────────────
name = "Alice"
age = 30

print(f"Hello, {name}. You are {age} years old.")

# ─── EXPRESSIONS INSIDE THE BRACES ──────────────────────
print(f"Next year: {age + 1}")               # arithmetic
print(f"Loud: {name.upper()}")                # method calls
print(f"Initial: {name[0]}")                  # indexing
print(f"Full: {name} ({age})")                # multiple vars

# ─── MULTI-LINE F-STRINGS ───────────────────────────────
report = f"""
── User Report ────────────────
  Name: {name}
  Age:  {age}
  Adult: {age >= 18}
"""
print(report)
OUTPUT
Hello, Alice. You are 30 years old. Next year: 31 Loud: ALICE Initial: A Full: Alice (30) ── User Report ──────────────── Name: Alice Age: 30 Adult: True

Format Specifiers — The Real Power

After a colon inside the braces, you can specify how to format the value: padding, alignment, decimal places, number bases, and more.

SpecMeaningExampleResult
.2f2 decimal places (float)f"{3.14159:.2f}"3.14
.0fNo decimals (round)f"{3.99:.0f}"4
,Thousands separatorf"{1000000:,}"1,000,000
,.2fThousands + 2 decimalsf"{1234.5:,.2f}"1,234.50
%Percentagef"{0.856:.1%}"85.6%
eScientific notationf"{1234567:.2e}"1.23e+06
bBinaryf"{10:b}"1010
oOctalf"{10:o}"12
x / XHex (lower/upper)f"{255:X}"FF
05dZero-pad to width 5f"{42:05d}"00042
<10Left-align, width 10f"{'hi':<10}|""hi |"
>10Right-align, width 10f"{'hi':>10}|"" hi|"
^10Center, width 10f"{'hi':^10}|"" hi |"
# ─── FORMATTING IN ACTION ───────────────────────────────

# Money formatting
price = 12345.678
print(f"Price: ${price:,.2f}")         # $12,345.68

# Percentage
pass_rate = 0.847
print(f"Pass rate: {pass_rate:.1%}")   # 84.7%

# Padded table columns
print(f"{'NAME':<10}{'AGE':>5}{'SCORE':>10}")
print(f"{'-'*25}")
print(f"{'Alice':<10}{30:>5}{95.5:>10.1f}")
print(f"{'Bob':<10}{25:>5}{88.2:>10.1f}")
print(f"{'Carol':<10}{35:>5}{92.7:>10.1f}")

# Debugging trick: = shows both name and value
x = 42
print(f"{x=}")                       # x=42

# Date formatting
from datetime import datetime
now = datetime.now()
print(f"Today: {now:%Y-%m-%d}")         # Today: 2026-07-09
print(f"Time:  {now:%H:%M:%S}")         # Time:  14:23:45
OUTPUT
Price: $12,345.68 Pass rate: 84.7% NAME AGE SCORE ------------------------- Alice 30 95.5 Bob 25 88.2 Carol 35 92.7 x=42 Today: 2026-07-09 Time: 14:23:45

Old Formatting Styles — For Reference Only

❌ Old Ways (avoid in new code)
StyleExample
% formatting"Hi, %s" % name
.format()"Hi, {}".format(name)
Concatenation"Hi, " + name
✅ The Modern Way (Python 3.6+)
StyleExample
f-stringf"Hi, {name}"
Fast~3× faster than .format()
ReadableVariables inline, no positional args

Section 10

Escape Characters & Special Sequences

Sometimes you need to include characters in a string that would otherwise confuse Python — quotes inside quotes, newlines, tabs, or actual backslashes. That is where escape sequences come in. They all start with a backslash \.

EscapeMeaningExampleOutput
\nNewline"Line1\nLine2"Line1
Line2
\tTab"A\tB"A    B
\\Literal backslash"C:\\Users"C:\Users
\'Single quote'it\'s'it's
\"Double quote"he said \"hi\""he said "hi"
\rCarriage returnRare — mostly Windows
\0Null characterLow-level binary data
\u00e9Unicode char"\u00e9"é
# ─── ESCAPING SPECIAL CHARACTERS ────────────────────────
print("Line 1\nLine 2\nLine 3")   # three lines
print("Name:\tAlice")              # tab between colon and Alice
print("She said \"hi\"")           # She said "hi"
print('It\'s fine')               # It's fine
print("C:\\Users\\Alice")          # C:\Users\Alice

# ─── RAW STRINGS DISABLE ESCAPING ───────────────────────
print(r"C:\Users\new_folder")      # C:\Users\new_folder (literal!)
print("C:\Users\new_folder")       # C:\Users
                                        #  ew_folder — \n interpreted!

# ─── UNICODE CHARACTERS ─────────────────────────────────
print("caf\u00e9")                # café
print("\N{GREEK SMALL LETTER PI}")  # π
print("\U0001F600")               # 😀 (emoji)
🔑
Windows Path Golden Rule

For Windows file paths, always use raw strings: r"C:\Users\Alice". Without the r, Python interprets \n, \t, \U etc. as escape sequences and your path silently breaks. Alternative: use forward slashes — they work on Windows too: "C:/Users/Alice".


Section 11

Iterating Over Strings

Since strings are sequences, you can loop over them character by character using a for loop. This is one of the most common patterns in string processing.

# ─── BASIC ITERATION ────────────────────────────────────
word = "Python"

for letter in word:
    print(letter)
# P
# y
# t
# h
# o
# n

# ─── ITERATION WITH INDEX (enumerate) ───────────────────
for i, letter in enumerate(word):
    print(f"Index {i}: {letter}")

# ─── COUNTING VOWELS ────────────────────────────────────
sentence = "The quick brown fox jumps"
vowels = "aeiouAEIOU"

count = 0
for char in sentence:
    if char in vowels:
        count += 1
print(f"Vowel count: {count}")     # Vowel count: 6

# ─── PYTHONIC VERSION USING SUM() ───────────────────────
count = sum(1 for c in sentence if c in vowels)
print(f"Vowel count: {count}")     # 6

Section 12

Complete Practice Program — String Toolkit

This program combines every concept from this tutorial: creation, indexing, slicing, methods, iteration, and f-strings — into one working script.

# string_toolkit.py — All string concepts in one program

def analyse_text(text):
    """Analyse a string and return a report dictionary."""
    return {
        "length":      len(text),
        "words":       len(text.split()),
        "upper":       text.upper(),
        "lower":       text.lower(),
        "reversed":    text[::-1],
        "first_word":  text.split()[0] if text.strip() else "",
        "last_word":   text.split()[-1] if text.strip() else "",
        "vowels":      sum(1 for c in text.lower() if c in "aeiou"),
        "digits":      sum(1 for c in text if c.isdigit()),
        "spaces":      text.count(" "),
    }

def format_report(text, stats):
    """Build a nicely formatted report using f-strings."""
    line = "═" * 50

    return f"""
{line}
  STRING ANALYSIS REPORT
{line}

  Input:      "{text[:40]}{'...' if len(text) > 40 else ''}"

  Length:     {stats['length']:>5} characters
  Words:      {stats['words']:>5}
  Vowels:     {stats['vowels']:>5}
  Digits:     {stats['digits']:>5}
  Spaces:     {stats['spaces']:>5}

  First word: {stats['first_word']}
  Last word:  {stats['last_word']}

  Uppercase:  {stats['upper'][:40]}
  Reversed:   {stats['reversed'][:40]}
{line}
"""

def main():
    # Sample text — try changing this!
    sample = "Python is a POWERFUL programming language, released in 1991."

    # Analyse and display
    stats = analyse_text(sample)
    print(format_report(sample, stats))

    # Bonus: check some properties using string methods
    print(f"  Contains 'Python'? {'Python' in sample}")
    print(f"  Starts with 'Py'?  {sample.startswith('Py')}")
    print(f"  Ends with '.'?     {sample.endswith('.')}")

    # Clean and rebuild sentence — the split+join pattern
    normalised = " ".join(sample.lower().split())
    print(f"\n  Normalised: {normalised}")

if __name__ == "__main__":
    main()
OUTPUT
══════════════════════════════════════════════════ STRING ANALYSIS REPORT ══════════════════════════════════════════════════ Input: "Python is a POWERFUL programming langua..." Length: 60 characters Words: 8 Vowels: 16 Digits: 4 Spaces: 7 First word: Python Last word: 1991. Uppercase: PYTHON IS A POWERFUL PROGRAMMING LANGUAGE Reversed: .1991 ni desaeler ,egaugnal gnimmargor ══════════════════════════════════════════════════ Contains 'Python'? True Starts with 'Py'? True Ends with '.'? True Normalised: python is a powerful programming language, released in 1991.

Section 13

Golden Rules

🐍 Python Strings — Non-Negotiable Rules
1
Never try to modify a string in place. s[0] = "X" will crash with TypeError. Instead, build a new string: s = "X" + s[1:]. This immutability is a feature, not a bug — it powers Python's speed and safety.
2
Remember: string methods return NEW strings. text.upper() does NOT change text. You must reassign: text = text.upper(). Forgetting this is the #1 beginner mistake with strings.
3
Slice end index is EXCLUSIVE. s[2:5] returns characters at indices 2, 3, 4 — not 5. This is the same convention as range() and every other Python sequence operation. Once you internalise it, slicing feels natural.
4
Use f-strings for all formatting. They are the fastest, most readable way to build strings in Python 3.6+. Ditch % formatting and .format() — they still work but read like legacy code.
5
Use raw strings for Windows paths and regex. r"C:\Users\Alice". Without the r, \U, \n, and \t become escape sequences and silently corrupt your path.
6
Prefer "".join(parts) over repeated + for building long strings. Because strings are immutable, s += "x" in a loop creates a new string every iteration — quadratic time. join() is O(n).
7
Use in for substring checks. "fox" in sentence is faster and cleaner than sentence.find("fox") != -1. Reserve find() for when you actually need the index.
8
Chain your string methods. text.strip().lower().replace(" ", "_") reads left-to-right like a pipeline. Since each method returns a new string, chaining is safe and idiomatic — a hallmark of clean Python code.