Quick summary
Summarize this blog with AI
Concatenation joins text values. Use + for a few strings, str.join() for a collection, and an f-string when values belong inside a sentence. Serialization formats such as CSV, URLs, and SQL have additional rules: concatenation alone does not handle them.
The examples run on Python 3.12.14 using only the standard library. Each example supplies its own inputs; SQL uses an in-memory SQLite database.
Combine a few strings, and convert numbers intentionally
name = "Ada"
count = 3
print("Hello, " + name + "!")
print(f"{name} has {count} tasks; total ${12.5:.2f}")
try:
print("tasks: " + count)
except TypeError:
print("TypeError: convert or format the number first")
Expected output:
Hello, Ada!
Ada has 3 tasks; total $12.50
TypeError: convert or format the number first
An f-string evaluates the expression and formats its result; it is not a general escaping mechanism. Use str(value) when the ordinary text representation is what you actually want. Converting None produces the word None; decide whether that is appropriate for the output rather than accidentally treating it as an empty string.
Join a collection with one separator policy
parts = ["red", "green", "blue"]
print(", ".join(parts))
print(repr("-".join([])))
print(" / ".join(str(n) for n in [10, 20, 30]))
try:
"-".join(["item", 3])
except TypeError:
print("join requires strings")
Expected output:
red, green, blue
''
10 / 20 / 30
join requires strings
The separator appears only between elements. An empty iterable produces an empty string. Joining does not ignore missing values or automatically stringify numbers. Repeatedly copying a growing immutable string can be expensive; collect fragments and join when that fits the task. Do not promise that one syntax is universally fastest: interpreter optimizations, input sizes, and allocation patterns affect measurements.
Use StringIO when code writes fragments incrementally
from io import StringIO
with StringIO() as buffer:
for item in ["alpha", "beta"]:
buffer.write(item)
buffer.write("\n")
text = buffer.getvalue()
print(repr(text))
print(buffer.closed)
Expected output:
'alpha\nbeta\n'
True
StringIO provides a text-file interface in memory. It does not make an unbounded report memory-safe: the entire output is still retained. For large exports, write directly to an appropriately managed file or stream. A context manager closes the buffer even if an operation raises.
Keep bytes, text, paths, and serialized records separate
import csv
import json
from io import StringIO
from pathlib import PurePosixPath
from urllib.parse import urlencode
print(b"caf\xc3\xa9".decode("utf-8") + "!")
print(PurePosixPath("reports") / "daily.csv")
print(urlencode({"q": "a&b", "page": 1}))
print(json.dumps({"note": 'He said "hi"'}, ensure_ascii=False))
output = StringIO(newline="")
csv.writer(output, lineterminator="\n").writerow(["a,b", 'say "hi"'])
print(repr(output.getvalue()))
Expected output:
café!
reports/daily.csv
q=a%26b&page=1
{"note": "He said \"hi\""}
'"a,b","say ""hi"""\n'
Decode bytes with the known source encoding; do not concatenate bytes with text. A path object handles separators, but it does not authorize an arbitrary user-supplied path. The URL encoder preserves an ampersand as part of a query value. CSV and JSON writers preserve embedded delimiters and quotes. For HTML text use the relevant template engine's escaping; a string operation does not make data safe for every destination.
Bind SQL values instead of concatenating them
import sqlite3
with sqlite3.connect(":memory:") as connection:
connection.execute("CREATE TABLE users (name TEXT NOT NULL)")
connection.executemany("INSERT INTO users VALUES (?)",
[("O'Brien",), ("Ada",)])
for name in ["O'Brien", "' OR 1=1 --"]:
rows = connection.execute(
"SELECT name FROM users WHERE name = ? ORDER BY name", (name,)
).fetchall()
print(rows)
print(connection.execute("SELECT COUNT(*) FROM users").fetchone()[0])
connection.close()
Expected output:
[("O'Brien",)]
[]
2
The question mark is SQLite's placeholder syntax. The one-element tuple supplies a value separately from the statement. An apostrophe and SQL-looking text stay data. Other drivers have their own binding syntax. Parameters do not represent table names, column names, or keywords: when those must vary, select from fixed application-owned statements or an explicit identifier allowlist. Homemade sanitizers, f-strings, and quote replacement are not substitutes for parameter binding.
Choose the operation by the destination
For a short human-readable message, use an f-string. For many text fragments with a common separator, use join. For a file-like writer, use StringIO or an actual output stream. For a structured format, use its serializer. For SQL, bind values. This keeps formatting decisions separate from the data contract and from security decisions.
References: str.join, StringIO, CSV, and SQLite parameter binding.