Python string concatenation

PYTHON Published Updated 3 mins read By Leon Wei
Python string concatenation cover image

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.

Interview Prep

Begin Your SQL, Python, and R Journey

Master 230 interview-style coding questions and build the data skills needed for analyst, scientist, and engineering roles.

Related Articles

All Articles
Python first match cover image
python Oct 17, 2026

Python first match

Master Python's First Match for string pattern searches. Efficiently locate the first occurrence of patterns in strings, a skill for log parsing…