Python Input and Output Practice Problems & Exercises
Practice: Input and Output
← Back to lessonEasy
Format the string "hello" and the number 42 using f-string alignment and padding. Match the expected output exactly — each line is wrapped in pipe characters to show the whitespace.
word = "hello"
num = 42
# Left-aligned in 10 chars
print(f"|{word:<10}|")
# Right-aligned in 10 chars
print(f"|{word:>10}|")
# Center-aligned in 10 chars
print(f"|{word:^10}|")
# Center-aligned with * fill in 10 chars
print(f"|{word:*^10}|")
# Left-aligned with . fill in 10 chars
print(f"|{word:.<10}|")
# Zero-padded integer in 5 digits
print(f"|{num:05d}|")Solution
|hello |
| hello|
| hello |
|***hello**|
|hello.....|
|00042|
Format spec anatomy: f"{value:fill_char alignment width type}"
<left-align (default for strings)>right-align (default for numbers)^center-align- Fill character goes before the alignment symbol —
*^10means "fill with*, center in 10 chars" 05dis shorthand for "zero-fill, 5-wide, decimal integer" — the0before the width triggers zero-padding
Common interview gotcha: Zero-padding (05d) only works with numeric types. Trying f"{'hello':05s}" raises a ValueError. For strings, use explicit fill: f"{'hello':0<5}".
Expected Output
|hello |\n| hello|\n| hello |\n|***hello**|\n|hello.....|\n|00042|Hints
Hint 1: f-string alignment uses < for left, > for right, ^ for center inside the format spec: f"{value:<width}".
Hint 2: You can specify a fill character before the alignment symbol: f"{value:*^10}" fills with asterisks. For zero-padding numbers, use f"{value:05d}".
Use print() with sep and end to produce the exact output below — no string concatenation allowed. Each line demonstrates a different technique.
# Line 1: Date with / separator
print(2024, "07", 15, sep="/")
# Line 2: Words joined by dashes
words = ["one", "two", "three", "four", "five"]
print(*words, sep="-")
# Line 3: Two prints on same line using end
print("Loading...", end="")
print("done!")
# Line 4: Items separated by " | "
print("A", "B", "C", sep=" | ")Solution
2024/07/15
one-two-three-four-five
Loading...done!
A | B | C
Key mechanics:
sepreplaces the default space betweenprint()arguments — useful for building delimited output withoutstr.join()endreplaces the default\nat the end —end=""keeps the cursor on the same line*wordsunpacks the list so each element becomes a separate argument toprint(), allowingsepto work between them- Default values:
sep=" "andend="\n"— you are overriding these defaults
Production tip: sep and end are faster than building a string with + or .join() for simple console output because they avoid creating intermediate string objects.
Expected Output
2024/07/15\none-two-three-four-five\nLoading...done!\nA | B | CHints
Hint 1: print() takes a sep parameter that replaces the default single space between arguments: print(a, b, c, sep="/").
Hint 2: The end parameter controls what goes at the end instead of a newline. Use end="" to suppress the newline, then a second print continues on the same line.
Print both str() and repr() for several values to see how they differ. Pay attention to how each handles strings, datetimes, and escape characters.
from datetime import datetime
# Strings
text = "hello world"
print(f"str: {str(text)}")
print(f"repr: {repr(text)}")
# Datetime
dt = datetime(2024, 7, 15, 10, 30)
print(f"str: {str(dt)}")
print(f"repr: {repr(dt)}")
# String with escape characters
escaped = "line1\nline2"
print(f"str: {escaped!s}")
print(f"repr: {escaped!r}")
# f-string conversion flags
print(f"f-string !s: {text!s}")
print(f"f-string !r: {text!r}")Solution
str: hello world
repr: 'hello world'
str: 2024-07-15 10:30:00
repr: datetime.datetime(2024, 7, 15, 10, 30)
str: line1
line2
repr: 'line1\nline2'
f-string !s: hello world
f-string !r: 'hello world'
The fundamental difference:
str()is for end users — pretty, readable, may lose informationrepr()is for developers — unambiguous, often valid Python to recreate the object
Key observations:
str("hello")returnshello(no quotes).repr("hello")returns'hello'(with quotes) so you can see it is a string.str(datetime)returns2024-07-15 10:30:00(ISO format).repr(datetime)returns the constructor call.str("line1\nline2")actually renders the newline.repr()shows the literal\nescape.
Debugging rule of thumb: Always use repr() (or !r in f-strings) when logging values — it reveals hidden whitespace, escape characters, and type information that str() hides.
Expected Output
str: hello world\nrepr: 'hello world'\nstr: 2024-07-15 10:30:00\nrepr: datetime.datetime(2024, 7, 15, 10, 30)\nstr: line1\\nline2\nrepr: 'line1\\nline2'\nf-string !s: hello world\nf-string !r: 'hello world'Hints
Hint 1: str() returns a human-readable version. repr() returns an unambiguous version that could recreate the object. For strings, repr() adds quotes around the value.
Hint 2: In f-strings, !s calls str() and !r calls repr(). The default (no flag) calls str(). Notice how repr shows escape characters literally.
Use format spec type codes to display the number 10 in different bases, format 3.14159265 to various precisions, and add thousand separators to 1234567.
n = 10
pi = 3.14159265
big = 1234567
fraction = 0.855
avogadro = 6.022e23
# Base conversions
print(f"Binary: {n:b}")
print(f"Octal: {n:o}")
print(f"Hex: {n:x}")
print(f"Hex upper: {n:X}")
# Float formatting
print(f"Float: {pi:f}") # default 6 decimal places
print(f"Fixed 2: {pi:.2f}") # 2 decimal places
# Percentage (multiplies by 100 automatically)
print(f"Percent: {fraction:.2%}")
# Scientific notation
print(f"Sci: {avogadro:e}")
# Thousand separators
print(f"Comma: {big:,d}")
print(f"Undscr: {big:_d}")Solution
Binary: 1010
Octal: 12
Hex: a
Hex upper: A
Float: 3.141593
Fixed 2: 3.14
Percent: 85.50%
Sci: 6.022000e+23
Comma: 1,234,567
Undscr: 1_234_567
Format spec type codes cheat sheet:
| Code | Meaning | Example |
|---|---|---|
b | Binary | 10 becomes 1010 |
o | Octal | 10 becomes 12 |
x/X | Hex lower/upper | 10 becomes a/A |
d | Decimal integer | Default for ints |
f | Fixed-point float | Default 6 decimals |
.Nf | N decimal places | .2f gives 2 places |
e/E | Scientific notation | 6.022e+23 |
% | Percentage | Multiplies by 100, adds % |
, | Comma separator | 1,234,567 |
_ | Underscore separator | 1_234_567 |
The % trap: The % format code multiplies the value by 100. So 0.855 becomes 85.50%, not 0.855%. If your value is already a percentage (like 85.5), divide by 100 first or just use f with a manual % suffix.
Expected Output
Binary: 1010\nOctal: 12\nHex: a\nHex upper: A\nFloat: 3.141593\nFixed 2: 3.14\nPercent: 85.50%\nSci: 6.022000e+23\nComma: 1,234,567\nUndscr: 1_234_567Hints
Hint 1: The format spec type codes are: b=binary, o=octal, x=hex lowercase, X=hex uppercase, f=fixed-point, e=scientific, %=percentage.
Hint 2: For thousand separators use , (comma) or _ (underscore) before the type code: f"{n:,d}" or f"{n:_d}". Percentage multiplies by 100 automatically.
Medium
Build a formatted table from the data below. Columns must be aligned: names left-aligned, ages right-aligned, salaries right-aligned with $ and commas, cities left-aligned.
employees = [
("Alice", 32, 120000, "New York"),
("Bob", 28, 95500, "San Francisco"),
("Charlie", 45, 180250, "Chicago"),
("Diana", 37, 145000, "Boston"),
]
# Header
header = f"{'Name':<15}{'Age':>5} {'Salary':<12} {'City'}"
print(header)
# Separator — dashes matching column widths
sep = f"{'----':<15}{'---':>5} {'------':<12} {'----'}"
print(sep)
# Data rows
for name, age, salary, city in employees:
salary_fmt = f"${salary:,}"
print(f"{name:<15}{age:>5} {salary_fmt:<12} {city}")Solution
Name Age Salary City
---- --- ------ ----
Alice 32 $120,000 New York
Bob 28 $95,500 San Francisco
Charlie 45 $180,250 Chicago
Diana 37 $145,000 Boston
Table formatting strategy:
- Define column widths up front — here: name=15, age=5, gap=4, salary=12, gap=1, city=variable
- Left-align text (
<) and right-align numbers (>) — this is how humans expect to scan tabular data - Format currency before aligning — build
$120,000first, then left-align the string in its column. If you try to right-align a number and prepend$, the dollar sign moves with the number, breaking visual alignment - Separator line uses the same widths — dashes under each header
Production alternative: For real applications, use the tabulate library or str.format_map() with computed widths. For quick debugging, this f-string approach is the fastest to write.
Expected Output
Name Age Salary City\n---- --- ------ ----\nAlice 32 $120,000 New York\nBob 28 $95,500 San Francisco\nCharlie 45 $180,250 Chicago\nDiana 37 $145,000 BostonHints
Hint 1: Use f-strings with field widths and alignment: f"{name:<15}" for left-aligned in 15 chars, f"{age:>5}" for right-aligned numbers.
Hint 2: For the salary column, format the number with comma separators and a dollar sign: f"${salary:>8,}". Build the separator line by repeating dashes to match column widths.
Create a Temperature class that responds to custom format specs: "f" for Fahrenheit, "c" for Celsius, "k" for Kelvin. It should also support precision like ".1f" or ".1c".
class Temperature:
"""Stores temperature in Fahrenheit internally."""
def __init__(self, fahrenheit: float):
self.f = fahrenheit
@property
def celsius(self) -> float:
return (self.f - 32) * 5 / 9
@property
def kelvin(self) -> float:
return self.celsius + 273.15
def __format__(self, spec: str) -> str:
# Parse precision and unit from spec
# Examples: "" -> default, "f" -> Fahrenheit, ".1c" -> Celsius 1 decimal
if not spec:
spec = ".2f" # default: Fahrenheit with 2 decimals
# Extract unit character (last char if it's a letter)
if spec[-1] in ('f', 'c', 'k'):
unit = spec[-1]
precision = spec[:-1] if len(spec) > 1 else ".2"
else:
unit = 'f'
precision = spec
# Select value and symbol based on unit
if unit == 'c':
value = self.celsius
symbol = "°C"
elif unit == 'k':
value = self.kelvin
symbol = "K"
else:
value = self.f
symbol = "°F"
# Format the number with the given precision
if not precision:
precision = ".2"
return f"{value:{precision}f}{symbol}"
def __repr__(self):
return f"Temperature({self.f})"
temp = Temperature(72.5)
print(f"Default: {temp}")
print(f"Fahrenheit: {temp:.2f}")
print(f"Celsius: {temp:.2c}")
print(f"Kelvin: {temp:.2k}")
print(f"Short F: {temp:.1f}")
print(f"Short C: {temp:.1c}")Solution
Default: 72.50°F
Fahrenheit: 72.50°F
Celsius: 22.50°C
Kelvin: 295.65K
Short F: 72.5°F
Short C: 22.5°C
How __format__ works:
- When Python encounters
f"{obj:spec}", it callsobj.__format__(spec)wherespecis the string after the colon - The spec is just a string — you parse it however you want. Here we split it into a precision part and a unit character
format(value, ".2f")delegates to the built-in float formatting for the numeric part
Design decisions:
- Default spec (
"") falls back to".2f"— Fahrenheit with 2 decimals, matching the most common use case - The unit code is the last character, so
".2c"means "2 decimal places, Celsius" - This piggybacks on the existing
f/etype codes convention, addingcandk
Why this matters: __format__ lets your objects integrate seamlessly with f-strings and the format() builtin. Libraries like datetime use this — f"{dt:%Y-%m-%d}" works because datetime.__format__ parses the %Y-%m-%d spec.
Expected Output
Default: 72.50°F\nFahrenheit: 72.50°F\nCelsius: 22.50°C\nKelvin: 295.65K\nShort F: 72.5°F\nShort C: 22.5°CHints
Hint 1: The __format__ method receives the format_spec string. Parse it to decide the output. For example, "c" for Celsius, "f" for Fahrenheit, "k" for Kelvin.
Hint 2: Split the format_spec into a precision part and a unit part. If spec is ".1c", the precision is ".1" and the unit is "c". Use the built-in format() on the number with the precision, then append the unit symbol.
Capture the output of a function that uses print() without modifying the function. Use io.StringIO and contextlib.redirect_stdout to intercept stdout, then analyze the captured text.
import io
from contextlib import redirect_stdout
def process_items(items):
"""A function that prints its progress — we can't modify it."""
for i, item in enumerate(items, 1):
print(f"Processing item {i}...")
print(f"Done: {len(items)} items processed.")
# Capture all print output into a StringIO buffer
buffer = io.StringIO()
with redirect_stdout(buffer):
process_items(["a", "b", "c"])
# Retrieve the captured output
captured = buffer.getvalue()
# Display and analyze
print("Captured output:")
print("---")
print(captured, end="") # end="" because captured already has trailing newline
print("---")
print(f"Line count: {len(captured.strip().splitlines())}")
print(f"Contains 'Done': {'Done' in captured}")Solution
Captured output:
---
Processing item 1...
Processing item 2...
Processing item 3...
Done: 3 items processed.
---
Line count: 4
Contains 'Done': True
Two approaches to capturing stdout:
Approach 1 — redirect_stdout context manager (recommended):
buffer = io.StringIO()
with redirect_stdout(buffer):
some_function() # All print() calls go to buffer
output = buffer.getvalue()
Approach 2 — print(file=...) (when you control the print calls):
buffer = io.StringIO()
print("hello", file=buffer)
output = buffer.getvalue()
When to use this:
- Testing: Capture function output to assert against expected text
- Logging: Intercept print statements from third-party code
- String building: Use print's formatting features (sep, end) but collect the result as a string instead of sending it to the console
Important: redirect_stdout is thread-local in CPython but not process-safe. In multi-threaded code, prefer passing a file= argument or using the logging module.
Expected Output
Captured output:\n---\nProcessing item 1...\nProcessing item 2...\nProcessing item 3...\nDone: 3 items processed.\n---\nLine count: 4\nContains 'Done': TrueHints
Hint 1: io.StringIO creates an in-memory file-like object. You can pass it as the file= argument to print(), or use contextlib.redirect_stdout to capture all stdout.
Hint 2: After capturing, call .getvalue() on the StringIO object to retrieve everything that was written to it as a single string.
Format numbers for a financial report. Currency should have $ and commas, negative amounts in parentheses. Percentages and scientific notation must have controlled precision.
revenue = 1234567.89
growth = 0.425
avogadro = 6.022e23
pi = 3.14159265
loss = -1234.56
big_growth = 12.5
tiny = 0.000000123
def format_currency(amount):
"""Format with $ and commas; negatives in parentheses."""
if amount < 0:
return f"(${abs(amount):,.2f})"
return f"${amount:,.2f}"
print(f"Revenue: {format_currency(revenue)}")
print(f"Growth: {growth:.2%}")
print(f"Atoms: {avogadro:.3e}")
print(f"Pi: {pi:.5e}")
print(f"Negative: {format_currency(loss)}")
print(f"Big pct: {big_growth:,.2%}")
print(f"Tiny: {tiny:.2e}")Solution
Revenue: $1,234,567.89
Growth: 42.50%
Atoms: 6.022e+23
Pi: 3.14159e+00
Negative: ($1,234.56)
Big pct: 1,250.00%
Tiny: 1.23e-07
Financial formatting patterns:
- Currency:
f"${amount:,.2f}"— comma thousands separator, 2 decimal places, manually prepend$ - Negative in parentheses: Accounting convention wraps negatives in
()instead of using-. Python does not do this natively, so you need a helper function - Percentage:
:.2%multiplies by 100 and appends%.0.425becomes42.50%,12.5becomes1,250.00% - Scientific:
:.3egives 3 decimal places in scientific notation. Useful for very large or very small numbers
The locale alternative: For production code handling multiple currencies and regional formats (e.g., 1.234.567,89 in Germany), use locale.currency() and locale.format_string(). The f-string approach is fine for US-formatted output.
Gotcha with %: It always multiplies by 100. If your data is already a percentage (like 42.5 meaning 42.5%), divide by 100 first or use f"{value:.2f}%" instead.
Expected Output
Revenue: $1,234,567.89\nGrowth: 42.50%\nAtoms: 6.022e+23\nPi: 3.14159e+00\nNegative: ($1,234.56)\nBig pct: 1,250.00%\nTiny: 1.23e-07Hints
Hint 1: For currency, format with comma separator and 2 decimal places, then prepend $. For negative currency in parentheses, check the sign and format accordingly.
Hint 2: The % format spec multiplies by 100 automatically. Scientific notation uses :e. Combine with precision: :.2e for 2 decimal places in scientific notation.
Build a text-based progress bar that updates in-place on a single line. Use \r (carriage return) and end="" to overwrite the previous output. The final state should print on its own line.
import sys
import time
def progress_bar(current, total, bar_width=20):
"""Return a progress bar string like [####----] 45% (9/20)"""
fraction = current / total
filled = int(bar_width * fraction)
bar = "#" * filled + "-" * (bar_width - filled)
percent = fraction * 100
return f"\r[{bar}] {percent:3.0f}% ({current}/{total})"
total_items = 50
# Simulate processing with progress updates
for i in range(1, total_items + 1):
sys.stdout.write(progress_bar(i, total_items))
sys.stdout.flush()
# In real code: time.sleep(0.05) — skipped here for speed
# Final line: overwrite with complete message and move to next line
print(f"\r[{'#' * 20}] 100% ({total_items}/{total_items}) Complete!")
print(f"Done! Processed {total_items} items.")Solution
[####################] 100% (50/50) Complete!
Done! Processed 50 items.
How carriage return (\r) works:
\rmoves the cursor to the beginning of the current line without creating a new line- The next
print()orwrite()overwrites whatever was on that line end=""orend="\r"preventsprint()from adding a newline, keeping the cursor on the same lineflush=True(orsys.stdout.flush()) forces the output buffer to display immediately — without it, the terminal may batch updates
The pattern:
write "[####----] 25%\r" -> cursor goes back to start
write "[########] 50%\r" -> overwrites previous text
write "[############] 75%\r" -> overwrites again
write "[################] 100%\n" -> final newline moves to next line
Production tips:
- Always make the progress bar fixed-width so shorter updates fully overwrite longer ones
- For serious progress bars, use
tqdm— it handles terminal width detection, ETA calculation, and nested bars - In Jupyter notebooks,
\rdoes not work well — useIPython.display.clear_output()ortqdm.notebookinstead
Expected Output
[####################] 100% (50/50) Complete!\nDone! Processed 50 items.Hints
Hint 1: Use print() with end="\r" to overwrite the same line. The carriage return moves the cursor to the beginning of the line without advancing to the next.
Hint 2: Build the bar string with a fixed width: filled = "#" * done_count, empty = "-" * remaining. Use sys.stdout.flush() or flush=True in print() to force immediate display.
Hard
Implement a JSON-style pretty-printer from scratch without using json.dumps. Handle dicts, lists, strings, numbers, booleans, and None. Output must match JSON formatting conventions (lowercase booleans, null for None, quoted strings).
def pretty_print(data, indent=0, indent_str=" "):
"""Pretty-print nested dicts/lists in JSON style."""
spacer = indent_str * indent
inner_spacer = indent_str * (indent + 1)
if isinstance(data, dict):
if not data:
return "{}"
lines = []
items = list(data.items())
for i, (key, value) in enumerate(items):
formatted_value = pretty_print(value, indent + 1, indent_str)
comma = "," if i < len(items) - 1 else ""
lines.append(f'{inner_spacer}"{key}": {formatted_value}{comma}')
return "{\n" + "\n".join(lines) + f"\n{spacer}" + "}"
elif isinstance(data, list):
if not data:
return "[]"
lines = []
for i, item in enumerate(data):
formatted_item = pretty_print(item, indent + 1, indent_str)
comma = "," if i < len(data) - 1 else ""
lines.append(f"{inner_spacer}{formatted_item}{comma}")
return "[\n" + "\n".join(lines) + f"\n{spacer}" + "]"
elif isinstance(data, str):
return f'"{data}"'
elif isinstance(data, bool):
return "true" if data else "false"
elif data is None:
return "null"
else:
return str(data)
# Test with nested data
data = {
"name": "Alice",
"age": 30,
"active": True,
"address": {
"street": "123 Main St",
"city": "Springfield",
"coords": [39.7817, -89.6501],
},
"scores": [95, 87, 92],
"metadata": None,
}
print(pretty_print(data))Solution
{
"name": "Alice",
"age": 30,
"active": true,
"address": {
"street": "123 Main St",
"city": "Springfield",
"coords": [
39.7817,
-89.6501
]
},
"scores": [
95,
87,
92
],
"metadata": null
}
Design decisions in the pretty-printer:
- Type dispatch order matters:
boolmust be checked beforeintbecauseisinstance(True, int)isTruein Python — booleans are a subclass of int - Recursive indentation: Each level increases the indent counter. The closing bracket uses the parent level's indent, creating the cascading visual structure
- Comma placement: Commas go after every item except the last. We track position with
enumerateand compare againstlen(items) - 1 - Empty containers:
{}and[]are returned as single-line strings — no need for newlines with no content
How json.dumps does it: json.dumps(data, indent=2) produces identical output. The standard library implementation handles additional edge cases: circular references, custom encoders via default=, Unicode escaping, and sorting keys. Our version demonstrates the core algorithm.
Extending this: To handle sets, tuples, dataclasses, or custom objects, add more isinstance branches. For circular reference detection, maintain a set() of id() values seen during recursion.
Expected Output
{
"name": "Alice",
"age": 30,
"active": true,
"address": {
"street": "123 Main St",
"city": "Springfield",
"coords": [
39.7817,
-89.6501
]
},
"scores": [
95,
87,
92
],
"metadata": null
}Hints
Hint 1: Handle each type separately: dict -> recurse with increased indent, list -> recurse each item, str -> wrap in quotes, bool -> lowercase, None -> "null", numbers -> as-is.
Hint 2: Track the indentation level and pass it down recursively. Use a base indent string (e.g., 2 spaces) multiplied by the current depth. Closing brackets go at the parent indent level.
Implement a format_value function that parses a format spec string and applies it manually — without calling the built-in format() for the final formatting. You must handle: fill, alignment, width, precision, type codes (d, f, s, x, X, b), sign, grouping (,), and the # alternate form.
def format_value(value, spec):
"""Parse and apply a format spec string manually."""
if not spec:
return str(value)
pos = 0
# --- Parse fill and align ---
fill = ' '
align = None
if len(spec) >= 2 and spec[1] in '<>^=':
fill = spec[0]
align = spec[1]
pos = 2
elif spec[0] in '<>^=':
align = spec[0]
pos = 1
# --- Parse sign ---
sign = '-' # default: only show minus
if pos < len(spec) and spec[pos] in '+-':
sign = spec[pos]
pos += 1
# --- Parse # (alternate form) ---
alt = False
if pos < len(spec) and spec[pos] == '#':
alt = True
pos += 1
# --- Parse 0-padding (implies fill='0', align='=') ---
if pos < len(spec) and spec[pos] == '0' and align is None:
fill = '0'
align = '='
pos += 1
# --- Parse width ---
width_str = ''
while pos < len(spec) and spec[pos].isdigit():
width_str += spec[pos]
pos += 1
width = int(width_str) if width_str else 0
# --- Parse grouping ---
grouping = ''
if pos < len(spec) and spec[pos] in ',_':
grouping = spec[pos]
pos += 1
# --- Parse .precision ---
precision = None
if pos < len(spec) and spec[pos] == '.':
pos += 1
prec_str = ''
while pos < len(spec) and spec[pos].isdigit():
prec_str += spec[pos]
pos += 1
precision = int(prec_str)
# --- Parse type ---
type_code = spec[pos] if pos < len(spec) else ''
# --- Convert value to string based on type ---
if type_code == 'd' or (type_code == '' and isinstance(value, int)):
num = int(value)
raw = str(abs(num))
is_negative = num < 0
elif type_code in ('x', 'X'):
num = int(value)
raw = hex(abs(num))[2:] # strip '0x'
if type_code == 'X':
raw = raw.upper()
is_negative = num < 0
elif type_code == 'b':
num = int(value)
raw = bin(abs(num))[2:] # strip '0b'
is_negative = num < 0
elif type_code == 'f':
num = float(value)
p = precision if precision is not None else 6
is_negative = num < 0
raw = f"{abs(num):.{p}f}"
elif type_code == 's' or (type_code == '' and isinstance(value, str)):
raw = str(value)
if precision is not None:
raw = raw[:precision]
is_negative = False
else:
raw = str(value)
is_negative = False
# --- Apply grouping for integer types ---
if grouping and type_code in ('d', ''):
parts = []
for i, ch in enumerate(reversed(raw)):
if i > 0 and i % 3 == 0:
parts.append(grouping)
parts.append(ch)
raw = ''.join(reversed(parts))
# --- Build sign string ---
if type_code in ('d', 'f', 'x', 'X', 'b', ''):
if is_negative:
sign_str = '-'
elif sign == '+':
sign_str = '+'
else:
sign_str = ''
else:
sign_str = ''
# --- Apply alternate form prefix ---
prefix = ''
if alt:
if type_code in ('x', 'X'):
prefix = '0x' if type_code == 'x' else '0X'
elif type_code == 'b':
prefix = '0b'
# --- Combine and pad ---
content = sign_str + prefix + raw
if not align:
align = '>' if isinstance(value, (int, float)) else '<'
if len(content) >= width:
return content
padding_needed = width - len(content)
if align == '<':
return content + fill * padding_needed
elif align == '>':
return fill * padding_needed + content
elif align == '^':
left_pad = padding_needed // 2
right_pad = padding_needed - left_pad
return fill * left_pad + content + fill * right_pad
elif align == '=':
# Padding between sign and digits
return sign_str + prefix + fill * padding_needed + raw
return content
# Test cases
tests = [
(42, '>10d'),
(42, '0>10d'),
(3.14, '^10.2f'),
('hi', '*<10s'),
(255, '#x'),
(255, '#X'),
(255, '#b'),
(1234567, ',d'),
(-42, '=+10d'),
(42, '+d'),
(-42, '+d'),
]
for value, spec in tests:
result = format_value(value, spec)
print(f"format_value({value}, '{spec}'){' ' * (16 - len(repr(value)) - len(spec))}= '{result}'")Solution
format_value(42, '>10d') = ' 42'
format_value(42, '0>10d') = '0000000042'
format_value(3.14, '^10.2f') = ' 3.14 '
format_value('hi', '*<10s') = 'hi********'
format_value(255, '#x') = '0xff'
format_value(255, '#X') = '0xFF'
format_value(255, '#b') = '0b11111111'
format_value(1234567, ',d') = '1,234,567'
format_value(-42, '=+10d') = '+ 42'
format_value(42, '+d') = '+42'
format_value(-42, '+d') = '-42'
The format spec grammar (PEP 3101):
format_spec ::= [[fill]align][sign][#][0][width][grouping][.precision][type]
fill ::= any character
align ::= '<' | '>' | '^' | '='
sign ::= '+' | '-' | ' '
width ::= integer
grouping ::= ',' | '_'
precision ::= integer
type ::= 'b'|'c'|'d'|'e'|'f'|'g'|'n'|'o'|'s'|'x'|'X'|'%'
Parsing strategy: Walk left-to-right. The tricky part is fill+align: if the second character is an alignment char, the first is fill. Otherwise check if the first character is an alignment char.
The = alignment is unique: it puts padding between the sign and digits. f"{-42:=+10d}" produces "- 42" — this is used in financial formatting where the sign must be flush-left and digits flush-right.
What we skipped: The full CPython implementation also handles 'n' (locale-aware), 'g' (general float), 'c' (character from int), and ' ' (space sign). Our parser covers the most common cases.
Expected Output
format_value(42, '>10d') = ' 42'
format_value(42, '0>10d') = '0000000042'
format_value(3.14, '^10.2f') = ' 3.14 '
format_value('hi', '*<10s') = 'hi********'
format_value(255, '#x') = '0xff'
format_value(255, '#X') = '0xFF'
format_value(255, '#b') = '0b11111111'
format_value(1234567, ',d') = '1,234,567'
format_value(-42, '=+10d') = '+ 42'
format_value(42, '+d') = '+42'
format_value(-42, '+d') = '-42'Hints
Hint 1: The full format spec grammar is: [[fill]align][sign][#][0][width][grouping][.precision][type]. Parse each part in order using character inspection.
Hint 2: Align characters are < > ^ =. Sign characters are + - (space). The # flag adds 0b/0o/0x prefixes. Implement formatting by converting the value to a string first, then applying padding.
Build a structured log formatter that outputs timestamped, level-tagged, source-tagged messages with fixed-width columns. Include the ability to capture output and filter by severity level.
import io
from datetime import datetime, timedelta
class LogFormatter:
"""Structured log formatter with timestamps, levels, and source tags."""
LEVELS = {'DEBUG': 10, 'INFO': 20, 'WARNING': 30, 'ERROR': 40, 'CRITICAL': 50}
def __init__(self, base_time=None):
self.base_time = base_time or datetime.now()
self.entries = []
self._offset_ms = 0
def _format_entry(self, timestamp, level, source, message):
"""Format a single log entry with fixed-width columns."""
ts_str = timestamp.strftime("%Y-%m-%d %H:%M:%S") + f".{timestamp.microsecond // 1000:03d}"
return f"[{ts_str}] [{level:<7}] [{source:<11}] {message}"
def log(self, level, source, message):
"""Log a message at the given level."""
timestamp = self.base_time + timedelta(milliseconds=self._offset_ms)
entry = {
'timestamp': timestamp,
'level': level,
'source': source,
'message': message,
'formatted': self._format_entry(timestamp, level, source, message),
}
self.entries.append(entry)
self._offset_ms += 100
return entry['formatted']
def info(self, source, message):
return self.log('INFO', source, message)
def debug(self, source, message):
return self.log('DEBUG', source, message)
def warning(self, source, message):
return self.log('WARNING', source, message)
def error(self, source, message):
return self.log('ERROR', source, message)
def get_entries(self, min_level='DEBUG'):
"""Filter entries by minimum severity level."""
threshold = self.LEVELS.get(min_level, 0)
return [
e['formatted'] for e in self.entries
if self.LEVELS.get(e['level'], 0) >= threshold
]
def format_all(self, min_level='DEBUG'):
"""Return all entries at or above min_level as a single string."""
return '\n'.join(self.get_entries(min_level))
# --- Demo ---
base = datetime(2024, 7, 15, 10, 30, 0)
logger = LogFormatter(base_time=base)
# Log various messages
logger.info('main', 'Application started')
logger.debug('db', 'Connection pool: 5 connections')
logger.warning('auth', 'Token expires in 300s')
logger.error('api', 'Request failed: status=503, endpoint=/health')
logger.info('main', 'Shutting down gracefully')
# Print all entries
print(logger.format_all())
# Filter to WARNING and above
print(f"\n--- Captured WARNING+ messages ---")
print(logger.format_all(min_level='WARNING'))Solution
[2024-07-15 10:30:00.000] [INFO ] [main ] Application started
[2024-07-15 10:30:00.100] [DEBUG ] [db ] Connection pool: 5 connections
[2024-07-15 10:30:00.200] [WARNING] [auth ] Token expires in 300s
[2024-07-15 10:30:00.300] [ERROR ] [api ] Request failed: status=503, endpoint=/health
[2024-07-15 10:30:00.400] [INFO ] [main ] Shutting down gracefully
--- Captured WARNING+ messages ---
[2024-07-15 10:30:00.200] [WARNING] [auth ] Token expires in 300s
[2024-07-15 10:30:00.300] [ERROR ] [api ] Request failed: status=503, endpoint=/health
Architecture of the log formatter:
-
Fixed-width columns using f-string alignment:
- Timestamp:
[YYYY-MM-DD HH:MM:SS.mmm]— 25 chars - Level:
[{level:<7}]— left-aligned in 7 chars (longest is "WARNING") - Source:
[{source:<11}]— left-aligned in 11 chars - Message: variable width, no padding
- Timestamp:
-
Level filtering via numeric values —
DEBUG=10 < INFO=20 < WARNING=30 < ERROR=40 < CRITICAL=50. Filtering toWARNING+means keep entries where level value is 30 or higher. -
Buffered output — entries are stored as dicts with both raw data and pre-formatted strings. This allows re-filtering without re-formatting.
How Python's logging module compares:
- Uses
LogRecordobjects instead of plain dicts - Formats via
Formatterclass with%-style or{-style template strings - Supports handlers (file, stream, socket, syslog) instead of a simple list
- Thread-safe with locks on each handler
Design pattern: This is the "Structured Logging" pattern — each entry is a structured record, not just a string. Production systems (structlog, loguru) take this further by emitting JSON records that log aggregators (ELK, Datadog) can parse and query.
Expected Output
[2024-07-15 10:30:00.000] [INFO ] [main ] Application started
[2024-07-15 10:30:00.100] [DEBUG ] [db ] Connection pool: 5 connections
[2024-07-15 10:30:00.200] [WARNING] [auth ] Token expires in 300s
[2024-07-15 10:30:00.300] [ERROR ] [api ] Request failed: status=503, endpoint=/health
[2024-07-15 10:30:00.400] [INFO ] [main ] Shutting down gracefully
--- Captured WARNING+ messages ---
[2024-07-15 10:30:00.200] [WARNING] [auth ] Token expires in 300s
[2024-07-15 10:30:00.300] [ERROR ] [api ] Request failed: status=503, endpoint=/healthHints
Hint 1: Create a Logger class that stores a list of outputs. Each log method formats the timestamp, level, and source into fixed-width columns. Use io.StringIO or a list to buffer messages.
Hint 2: For filtering by level, assign numeric values to levels (DEBUG=10, INFO=20, WARNING=30, ERROR=40) and filter entries where the stored level >= threshold.
