---
title: Strings in Python
source: https://app.sythra.ai/learn/python/strings-in-python
topic: Python
updated: 2026-08-12
publisher: Sythra (https://app.sythra.ai)
---

# Strings in Python

A Python string is an immutable sequence of characters. You access characters with zero-based indexing (fruit[0]), extract ranges with slicing (fruit[1:4]), and use built-in methods like .upper(), .find(), and .replace() to work with text.

_Source: [https://app.sythra.ai/learn/python/strings-in-python](https://app.sythra.ai/learn/python/strings-in-python) — free to read on Sythra._

## Key points

- Strings are sequences — index with fruit[0], starting at zero
- Negative indices (fruit[-1]) count backward from the end
- Slicing fruit[n:m] excludes the end index m
- Strings are immutable — you build new strings, never edit in place
- Methods like .upper(), .lower(), .strip(), .replace(), .find() operate on strings
- The in operator checks substrings; comparisons are alphabetical (case-sensitive)

Most of the data you meet early in Python is numbers — integers, floats, results of calculations. But a huge share of real programming is **text**: names, messages, passwords, sentences, entire files. In Python, all of that text lives inside a **string**.

This page goes past "strings are things you print" and treats them as what they really are — **sequences** you can index, slice, search, and rebuild. Pair it with [Data Types in Python](/learn/python/data-types) for where `str` fits among Python's other built-in types.

## What you will learn

By the end you can:

- Index into a string and explain why counting starts at **zero**
- Use `len()` and negative indices without off-by-one mistakes
- Slice out any chunk of a string with `[n:m]`
- Explain why strings are **immutable** in Python
- Search, count, and compare strings, and use core string methods

## A string is a sequence

A string is not really "one thing" — it is a **sequence** of individual characters, lined up one after another. Because it is a sequence, you can reach in and pull out any single character using square brackets:

```python
fruit = 'banana'
letter = fruit[1]
print(letter)
```

You might expect this to print `b`, the first letter. It actually prints `a` — one of programming's most disorienting moments for almost every beginner. In Python, **indexing starts at zero, not one**. Think of the index as an _offset from the beginning_, not a position you count on your fingers.

```python
fruit[0]   # 'b'
fruit[1]   # 'a'
fruit[2]   # 'n'
fruit[3]   # 'a'
fruit[4]   # 'n'
fruit[5]   # 'a'
```

Zero steps from the start means you have not moved at all — you are already standing on the first character. The index must always be a whole number; a decimal is refused outright:

```python
fruit[1.5]
# TypeError: string indices must be integers, not float
```

## len() and negative indices

The built-in `len()` function tells you exactly how many characters a string holds:

```python
fruit = 'banana'
len(fruit)   # 6
```

Six characters, indexed `0` through `5`. This sets up a classic trap: since indexing starts at `0`, the **last** character sits at index `5`, not `6`.

```python
last = fruit[len(fruit)]       # IndexError: string index out of range
last = fruit[len(fruit) - 1]   # 'a'  (correct)
```

Python gives you a cleaner fix for this exact problem — **negative indices**, which count backward from the end:

```python
fruit[-1]   # 'a'  (last character)
fruit[-2]   # 'n'  (second to last)
fruit[-6]   # 'b'  (first character)
```

## Walking through a string with a for loop

Going through a string one character at a time is called **traversal**. A `while` loop can do it, but it requires managing an index by hand:

```python
index = 0
while index < len(fruit):
    letter = fruit[index]
    print(letter)
    index = index + 1
```

A `for` loop does the same thing far more cleanly — no index variable, no off-by-one risk:

```python
for char in fruit:
    print(char)
```

Each pass, `char` automatically becomes the next character in line, and the loop ends on its own once nothing is left. Traversal combines naturally with **concatenation** (joining strings with `+`):

```python
prefixes = 'JKLMNOPQ'
suffix = 'ack'

for letter in prefixes:
    print(letter + suffix)
# Jack
# Kack
# Lack
# Mack
# Nack
# Oack
# Pack
# Qack
```

(`Oack` and `Qack` are not real English spellings — this simple loop does not know about the special-case rules for `O` and `Q`. The point is seeing how naturally a loop builds new strings by gluing pieces together.)

## String slices — cutting out a piece

Just like a single index gets one character, a **slice** gets an entire chunk at once:

```python
s = 'Monty Python'
s[0:5]    # 'Monty'
s[6:12]   # 'Python'
```

`[n:m]` means every character from index `n` up to — but **not including** — index `m`. This has a useful side effect: `s[0:5]` gives exactly 5 characters, and `s[5:12]` picks up right where it left off, clean and chainable.

Leave out the first number to start from the beginning; leave out the second to run to the end:

```python
fruit = 'banana'
fruit[:3]    # 'ban'     (start through index 2)
fruit[3:]    # 'ana'     (index 3 to the end)
fruit[:]     # 'banana'  (the whole string)
```

If the first index is greater than or equal to the second, you get an **empty string** — a valid string with zero characters:

```python
fruit[3:3]   # ''
```

## Strings are immutable

You might expect to change a single character the way you originally assigned one, by putting the bracket on the left of `=`. Python refuses:

```python
greeting = 'Hello, world!'
greeting[0] = 'J'
# TypeError: 'str' object does not support item assignment
```

Strings in Python are **immutable** — once created, they can never be modified in place. What you build instead is a brand-new string from pieces of the old one:

```python
greeting = 'Hello, world!'
new_greeting = 'J' + greeting[1:]
print(new_greeting)   # 'Jello, world!'
```

The original `greeting` is untouched; `new_greeting` is a completely separate string. This is not a limitation — strings that cannot secretly change underneath you are safer and easier to reason about.

## Searching inside a string

Here is a function that finds where a character first appears — the reverse of indexing: give it a character, get back a position.

```python
def find(word, letter):
    index = 0
    while index < len(word):
        if word[index] == letter:
            return index
        index = index + 1
    return -1
```

If the character never appears, the function returns `-1` — a common convention for "not found." Notice the `return` inside the loop: the function exits the moment a match is found, with no need to keep checking. This pattern — walk a sequence, return the moment you find what you want — is called a **search**, and you will reuse this shape constantly.

## Looping and counting

Counting how many times something appears is another everyday pattern:

```python
word = 'banana'
count = 0
for letter in word:
    if letter == 'a':
        count = count + 1
print(count)   # 3
```

`count` starts at zero and increases by one every time `'a'` shows up. This pattern is called a **counter**, and you will use it for counting characters, matches, or anything that satisfies a condition.

## String methods — functions that belong to strings

Everything so far has used `function_name(argument)`. Python also has **methods** — functions permanently attached to a specific type, called with a dot instead of standing alone:

```python
word = 'banana'
new_word = word.upper()
print(new_word)   # 'BANANA'
```

Instead of `upper(word)`, you write `word.upper()` — this is called **invoking a method** on an object. Python's built-in `.find()` method does what the handwritten `find` function above does, and more:

```python
word = 'banana'
word.find('a')        # 1   (first occurrence)
word.find('na')       # 2   (whole substrings, not just single letters)
word.find('na', 3)    # 4   (start searching from index 3)
word.find('b', 1, 2)  # -1  (only search between index 1 and 2)
```

A few more string methods worth knowing right away:

```python
word.upper()            # 'BANANA'  — all uppercase
word.lower()            # 'banana'  — all lowercase
word.strip()            # removes whitespace from both ends
word.replace('a', 'o')  # 'bonono'  — replaces every 'a' with 'o'
```

The full list lives in Python's own documentation, but `.strip()` and `.replace()` show up constantly and are worth remembering early.

## The in operator

`in` checks whether one string appears inside another, returning `True` or `False`:

```python
'a' in 'banana'      # True
'seed' in 'banana'   # False
'nan' in 'banana'    # True  (whole substrings work too)
```

This makes some code read almost like plain English:

```python
def in_both(word1, word2):
    for letter in word1:
        if letter in word2:
            print(letter)
```

Read it out loud: _"For each letter in word1, if the letter is in word2, print it."_ That is almost exactly what the code does.

## Comparing strings

`==`, `!=`, `<`, and `>` all work on strings. Python compares them alphabetically (technically by Unicode value, but alphabetical is the right mental model for now):

```python
if word == 'banana':
    print('All right, bananas.')

if word < 'banana':
    print('Your word comes before banana.')
elif word > 'banana':
    print('Your word comes after banana.')
else:
    print('All right, bananas.')
```

> **Watch out for case:** 

## Debugging: off-by-one errors with indices

Index bugs are one of the single most common sources of errors in Python. The most frequent mistake: being **off by one** — starting or ending your counting in the wrong place.

```python
def is_reverse(word1, word2):
    if len(word1) != len(word2):
        return False
    i = 0
    j = len(word2)        # bug hiding here
    while j > 0:
        if word1[i] != word2[j]:    # and here too
            return False
        i = i + 1
        j = j - 1
    return True
```

Calling `is_reverse('pots', 'stop')` throws an `IndexError`. `j` starts at `len(word2)`, which is `4` — but valid indices for a 4-character string only run `0` through `3`. The fix: `j = len(word2) - 1`.

The debugging habit worth keeping for life: **print right before the line that crashes**, to see exactly what your variables hold at that moment.

```python
while j > 0:
    print(i, j)    # what are these values right now?
    if word1[i] != word2[j]:
        ...
```

That output would immediately reveal `j` sitting at `4` — out of range for a 4-character word. Once you know the exact values that caused the crash, figuring out why is usually straightforward.

## Common mistakes

- Expecting `fruit[1]` to be the first character (it's the second — indexing starts at 0)
- Using `fruit[len(fruit)]` to grab the last character instead of `fruit[-1]` or `fruit[len(fruit) - 1]`
- Trying to assign into a string like `greeting[0] = 'J'` — strings are immutable
- Forgetting slices exclude the end index — `s[0:5]` stops before index 5
- Comparing strings without normalizing case first

## Why this matters

Strings show up in nearly every real program — parsing input, validating data, building messages, processing files. Understanding them as indexable, sliceable, immutable sequences — not just "text you print" — is what lets you manipulate real-world data with confidence.

> **Practice with Sythra:**  [Practice with AI tutor](https://app.sythra.ai/pricing)

## FAQ

### Why does Python string indexing start at 0?

Python indices represent an offset from the start of the sequence, not a position count. Index 0 means zero steps from the beginning, so the first character is at index 0, the second at index 1, and so on.

### How do you slice a string in Python?

Use s[n:m] to get characters from index n up to but not including index m. Leaving out n starts from the beginning; leaving out m runs to the end, e.g. s[:3] or s[3:].

### Are strings mutable or immutable in Python?

Strings are immutable. You cannot change a character in place (greeting[0] = 'J' raises a TypeError). Instead, you build a new string from pieces of the old one.

### How do you find a substring in a Python string?

Use the .find() method, e.g. word.find('na'), which returns the index of the first match or -1 if not found. The in operator ('na' in word) returns True or False instead.

### How do you check if a string contains another string in Python?

Use the in operator: 'seed' in 'banana' returns False, while 'nan' in 'banana' returns True. This works for whole substrings, not just single characters.

### Why does string comparison give unexpected results in Python?

Python compares strings by Unicode value, and all uppercase letters rank before all lowercase letters. So 'Pineapple' < 'banana' is True. Convert both sides with .lower() before comparing to avoid this.

## Related

- [Data Types in Python](https://app.sythra.ai/learn/python/data-types) — Where str fits among Python's built-in types.
- [The input() Function in Python](https://app.sythra.ai/learn/python/python-input-function) — input() always returns a string — this page explains what that means.
- [Operators and Operands in Python](https://app.sythra.ai/learn/python/operators-operands) — How + and comparison operators behave on strings.
- [Types, Values, and Errors in Python](https://app.sythra.ai/learn/python/types-values-errors) — The type() function and how errors like TypeError work.
- [Lists in Python](https://app.sythra.ai/learn/python/lists-in-python) — Mutability, list methods, map/filter/reduce, and the aliasing trap.
- [Dictionaries in Python](https://app.sythra.ai/learn/python/dictionaries-in-python) — Key-value pairs, the histogram pattern, and memoization.
- [Tuples in Python](https://app.sythra.ai/learn/python/tuples-in-python) — Immutability, unpacking, *args, zip(), and DSU sorting.
- [Python course hub](https://app.sythra.ai/learn/python) — All free Python explainers and the path into Agentic practice.

---

Written by Sythra — Learn machine learning by building. Practice this topic with Sythra's AI tutor: https://app.sythra.ai/pricing
