Strings, UTF-8 Encoding, and Slicing

Updated

September 7, 2026

Strings, UTF-8 Encoding, and Slicing

After reading this chapter, you will understand how CPython represents Unicode text, master string slicing mechanics, and author clean modern f-strings.

Mental model

In Python 3, str is an immutable sequence of Unicode code points. CPython internally optimizes string storage (PEP 393: Flexible String Representation) using the smallest necessary byte width per character: - Pure ASCII strings use 1 byte per character (Latin-1). - Extended multilingual strings use 2 bytes (UCS-2). - Emojis and complex scripts use 4 bytes (UCS-4).

String Slicing Mechanics:
   String:   P   y   t   h   o   n
 + Index:    0   1   2   3   4   5
 - Index:   -6  -5  -4  -3  -2  -1

 Slice Syntax: s[start:stop:step]
 Interval:     Half-open [start, stop) — includes start, excludes stop.

Minimal example

Save as string_slicing.py:

# string_slicing.py
def main() -> None:
    text = "telemetry_prod_us_east_1"

    # Slicing
    prefix = text[:9]       # From start to index 9 (exclusive)
    env = text[10:14]       # "prod"
    suffix = text[-9:]      # Last 9 characters
    reversed_text = text[::-1] # Step -1 reverses sequence

    print(f"Original : {text}")
    print(f"Prefix   : {prefix}")
    print(f"Env      : {env}")
    print(f"Suffix   : {suffix}")
    print(f"Reversed : {reversed_text}")

    # Modern f-string debugging specifier (=)
    latency_ms = 4.2819
    print(f"\nDebugging format: {latency_ms=:.2f}")

if __name__ == "__main__":
    main()

Run via uv run python string_slicing.py:

Original : telemetry_prod_us_east_1
Prefix   : telemetry
Env      : prod
Suffix   : us_east_1
Reversed : 1_tsae_su_dorp_yrtmelet

Debugging format: latency_ms=4.28

Worked examples

Case 1: String building performance (join vs +=)

Strings are immutable. Concatenating strings with += in a loop creates a new string and copies data on every iteration (\(O(N^2)\) time). Using ''.join() is \(O(N)\).

# string_builder.py
import time

def benchmark() -> None:
    n = 100_000
    words = ["chunk"] * n

    # Idiomatic O(N) method
    start = time.perf_counter()
    result = "".join(words)
    duration = time.perf_counter() - start
    print(f"join() duration: {duration:.4f}s (length={len(result)})")

if __name__ == "__main__":
    benchmark()

Run:

uv run python string_builder.py

Case 2: Encoding strings to bytes and decoding back

When sending data over network sockets or files, you must explicitly encode str (characters) to bytes (raw binary):

# unicode_codec.py
def main() -> None:
    original = "Microservices 🚀 [Ξs]"

    # UTF-8 serialization
    encoded_bytes = original.encode("utf-8")
    print(f"Encoded bytes ({len(encoded_bytes)} bytes):\n{encoded_bytes}")

    # Deserialization
    decoded_str = encoded_bytes.decode("utf-8")
    print(f"\nDecoded str: {decoded_str}")
    print(f"Match: {original == decoded_str}")

if __name__ == "__main__":
    main()

Run:

uv run python unicode_codec.py

Pitfalls

Pitfall 1: Confusing bytes and str

str and bytes cannot be implicitly combined. Concatenating b"hello" + "world" raises an immediate TypeError.

Pitfall 2: Slice out-of-range behavior

Index lookups like s[100] raise IndexError. However, slices like s[100:200] do not fail; they return an empty string "".


Exercises

  1. Write a function is_palindrome(s: str) -> bool that strips non-alphanumeric characters, converts to lowercase, and checks if the string equals its reverse slice (s[::-1]).
  2. Parse an ISO timestamp string "2026-09-06T20:15:30Z" into separate date and time components using slicing.
  3. Format a tabular output using f-string alignment specifiers: left-aligned 20 chars (<20), right-aligned 10 chars (>10), and float formatted to 2 decimals.

Further reading

  • PEP 393: Flexible String Representation.
  • Python Standard Library: Text Sequence Type — str.
  • Ned Batchelder: Pragmatic Unicode.