Process Unicode text, encodings, safe paths, and structured files with explicit, reliable boundaries.
Text crosses important program boundaries: user input, URLs, configuration, logs, files, and network messages. Python str values represent Unicode text, bytes represent encoded binary data, and pathlib.Path represents filesystem locations. Reliable programs keep those meanings separate, normalize only when a rule requires it, and make every encoding and path boundary explicit.
Learning goals
Treat strings as immutable Unicode sequences.
Search, split, join, replace, normalize, and format text deliberately.
Distinguish text from encoded bytes and choose UTF-8 explicitly.
Construct and inspect paths with pathlib instead of manual separators.
Read, write, append, and stream files with a clear lifecycle.
Handle missing files, malformed text, unsafe names, and structured formats at explicit boundaries.
Model text with Unicode strings
A Python string is an immutable sequence of Unicode code points. Quotes delimit the source text but are not part of its value. Single and double quotes behave alike; triple-quoted strings can span lines. Prefix r creates a raw string literal for backslashes, while prefix f enables expression interpolation.
PYTHON · EDITABLE
learner ="Amina"
city ='Casablanca'
greeting =f"Hello, {learner} from {city}"
multiline ="""First line
Second line"""print(greeting)print(len(multiline))print(multiline.splitlines())
A visible character is not always one code point: accents can be represented in more than one Unicode form, and emoji may combine several code points. len counts code points rather than user-perceived grapheme clusters. Most application text can stay as str, but search and equality rules may require deliberate Unicode normalization or case folding.
Transform immutable strings
String indexing and slicing follow sequence rules, but individual positions cannot be replaced. Methods such as strip, lower, upper, replace, and removeprefix return new strings. The original value stays unchanged unless you explicitly rebind its name.
Use in for containment, startswith and endswith for boundary checks, and find when -1 is a meaningful not-found result. index raises ValueError when missing. count reports non-overlapping occurrences. For case-insensitive human text, casefold is more comprehensive than lower, although locale-specific rules may still require specialized handling.
PYTHON · EDITABLE
filename ="Study.NOTES.txt"
query ="notes"print(filename.casefold().endswith(".txt"))print(query.casefold()in filename.casefold())print(filename.find(".txt"))ifnot filename.strip():raise ValueError("filename must not be blank")
Split structured text and join results
split separates a string around a delimiter or runs of whitespace. splitlines handles common line boundaries without leaving newline characters by default. partition always returns a three-item tuple and is useful when one separator divides a key from a value. join belongs to the separator string and combines an iterable of strings efficiently.
PYTHON · EDITABLE
raw_tags =" python, files, unicode ,, paths "
tags =[
part.strip()for part in raw_tags.split(",")if part.strip()]
label =" · ".join(tags)
key, separator, value ="theme=dark".partition("=")print(tags)print(label)print(key, separator, value)
Format output without changing the data
F-strings evaluate expressions inside braces and apply the format mini-language after a colon. Keep numbers and dates in useful data types while calculating, then format them at the presentation boundary. repr or the !r conversion is useful for diagnostics because whitespace and escape characters remain visible.
Avoid constructing SQL, shell commands, HTML, or URLs by merely inserting untrusted text into an f-string. Each destination has its own escaping or parameterization rules. Formatting creates text; it does not make that text safe for another language.
Cross the text and bytes boundary explicitly
Files and networks ultimately store bytes. Encoding converts str to bytes; decoding converts bytes to str. UTF-8 is the usual portable default, but a program must follow the actual external format. A mismatched codec can raise UnicodeEncodeError or UnicodeDecodeError—or silently produce corrupted text when errors are discarded.
Path objects combine path components with /, expose names and suffixes, and provide readable file operations. They use the current operating system's path rules, unlike manual string concatenation. A relative path is interpreted from the process's current working directory; an absolute path starts from a filesystem root.
Path.resolve produces an absolute normalized path and follows filesystem semantics. exists, is_file, and is_dir inspect the current filesystem but can become stale before a later operation. Perform the operation and handle its failure rather than assuming a prior check guarantees success.
Keep untrusted paths inside an allowed directory
A user-controlled filename may contain .., an absolute path, separators, or symbolic-link behavior that escapes an intended directory. Resolve the base and candidate, then verify that the final candidate is within the base before reading or writing. Path.name is useful only when the product intentionally discards every supplied directory component.
Path.read_text and write_text are concise for files that comfortably fit in memory. Specify encoding='utf-8' for portable text. write_text replaces an existing file and returns the number of characters written. Create parent directories deliberately before writing; do not assume they exist.
open returns a file object. A with block closes it reliably when the block finishes, including when an exception is raised. Mode r reads, w truncates then writes, a appends, and x creates only when the path does not exist. Add b for binary data; text mode performs encoding and newline translation.
read and read_text load the entire file into memory. Iterating over a text file produces lines lazily and is a better default for large logs or datasets. Each line usually includes its newline; use rstrip('\n') when you only intend to remove the line ending, rather than strip(), which also removes meaningful surrounding spaces.
PYTHON · EDITABLE
from pathlib import Path
path = Path("scores.txt")
path.write_text("84\n61\n93\n", encoding="utf-8")
total =0
count =0with path.open(encoding="utf-8")asfile:for line_number, line inenumerate(file, start=1):
text = line.rstrip("\n")try:
score =int(text)except ValueError as error:raise ValueError(f"invalid score on line {line_number}")from error
total += score
count +=1print(total / count)
path.unlink()
Use parsers for structured formats
Do not parse CSV by splitting on commas: quoted fields may contain commas and newlines. Use csv. Do not build or interpret JSON with string replacement: use json. A parser handles syntax, but your program must still validate the resulting shape, required fields, types, ranges, and size limits.
PYTHON · EDITABLE
import json
raw ='{"course": "Python", "completed": 7}'
data = json.loads(raw)ifnotisinstance(data,dict):raise TypeError("document must be an object")ifnotisinstance(data.get("course"),str):raise ValueError("course must be text")
output = json.dumps(data, ensure_ascii=False, indent=2)print(output)
Handle filesystem failures narrowly
File operations may fail because a path is missing, already exists, is a directory, has invalid text, or lacks permission. Catch the narrow exception you can recover from—such as FileNotFoundError for an optional file—and let unexpected failures retain their evidence. Include the relevant safe path or line number when translating an error.
PYTHON · EDITABLE
from pathlib import Path
defload_optional_notes(path: Path)->str:try:return path.read_text(encoding="utf-8")except FileNotFoundError:return""except UnicodeDecodeError as error:raise ValueError(f"{path.name} is not valid UTF-8 text")from error
Common text and file mistakes
Assuming string methods mutate the original value.
Normalizing human text without a product-specific rule.
Confusing Unicode text with encoded bytes.
Relying on the platform's default file encoding.
Joining paths with hard-coded slash characters.
Trusting a user-supplied filename or allowing it to escape the intended directory.
Loading an unbounded file entirely into memory.
Using strip when only the newline should be removed.
Parsing CSV, JSON, HTML, or commands with ad hoc string operations.
Build and verify a UTF-8 notes report
Run the notebook below in the browser's isolated filesystem. Predict the cleaned notes and report text, then add Arabic text or emoji, blank lines, and surrounding spaces. Confirm the UTF-8 round trip and inspect the validation error for an empty notes collection.
Identify every boundary where text is normalized.
Explain why join is used instead of repeated string concatenation.
Confirm the file is closed before it is read back.
Change the report path with Path.with_name or with_suffix.
Keep the final cleanup inside the isolated notebook example.
Lesson review
You can now transform immutable Unicode strings, preserve the distinction between text and bytes, construct paths portably, contain untrusted names, read and write UTF-8 text, stream large files, parse structured formats, and handle expected filesystem failures precisely. Complete the quick checks and scored quiz before moving to exceptions and modules.
RUNNABLE PYTHON NOTEBOOK
Build and verify a UTF-8 notes report
Normalize meaningful text, write and read a UTF-8 report in the isolated browser filesystem, verify the result, and clean up the temporary file.
from pathlib import Path
defclean_notes(raw_notes:list[str])->list[str]:return[
note.strip()for note in raw_notes
if note.strip()]defbuild_report(title:str, raw_notes:list[str])->str:
notes = clean_notes(raw_notes)ifnot notes:raise ValueError("at least one note is required")
numbered =[f"{position}. {note}"for position, note inenumerate(notes, start=1)]returnf"{title.strip()}\n\n"+"\n".join(numbered)+"\n"
raw_notes =[" Review functions ","","Practice pathlib 🐍"," مرحبا Python ",]
report_text = build_report("Weekly study notes", raw_notes)
report_path = Path("study-report.txt")try:
report_path.write_text(report_text, encoding="utf-8")
restored = report_path.read_text(encoding="utf-8")print(restored)print("UTF-8 round trip:", restored == report_text)print("Report lines:",len(restored.splitlines()))finally:if report_path.exists():
report_path.unlink()assert clean_notes([" A ",""," B "])==["A","B"]
OUTPUT
Press Run code to execute this Python program in your browser.
QUICK CHECK
Test what you learned
Which encoding should portable course text files specify explicitly?
QUICK CHECK
Test what you learned
Which standard-library class represents filesystem paths as objects?
QUICK CHECK
Test what you learned
Which statement guarantees an opened file is closed when its block finishes?
KNOWLEDGE QUIZ
Check your text-and-files mental model
Choose an answer for every question, then check the explanations. The goal is not to memorize syntax; it is to predict what Python will do and why.