Skip to main content

Python Input and Output Practice Problems & Exercises

Practice: Input and Output

12 problems4 Easy5 Medium3 Hard40–55 min
← Back to lesson

Easy

#1F-String Padding and AlignmentEasy
f-stringpaddingalignmentformat-spec

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.

Python
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 — *^10 means "fill with *, center in 10 chars"
  • 05d is shorthand for "zero-fill, 5-wide, decimal integer" — the 0 before 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}".

#2Print Separators and Line EndingsEasy
printsependoutput-control

Use print() with sep and end to produce the exact output below — no string concatenation allowed. Each line demonstrates a different technique.

Python
# 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:

  • sep replaces the default space between print() arguments — useful for building delimited output without str.join()
  • end replaces the default \n at the end — end="" keeps the cursor on the same line
  • *words unpacks the list so each element becomes a separate argument to print(), allowing sep to work between them
  • Default values: sep=" " and end="\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 | C
Hints

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.

#3repr() vs str() — Spot the DifferenceEasy
reprstrdebuggingstring-representation

Print both str() and repr() for several values to see how they differ. Pay attention to how each handles strings, datetimes, and escape characters.

Python
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 information
  • repr() is for developers — unambiguous, often valid Python to recreate the object

Key observations:

  • str("hello") returns hello (no quotes). repr("hello") returns 'hello' (with quotes) so you can see it is a string.
  • str(datetime) returns 2024-07-15 10:30:00 (ISO format). repr(datetime) returns the constructor call.
  • str("line1\nline2") actually renders the newline. repr() shows the literal \n escape.

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.

#4Format Spec Mini-Language BasicsEasy
format-specnumber-formattingtype-codes

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.

Python
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:

CodeMeaningExample
bBinary10 becomes 1010
oOctal10 becomes 12
x/XHex lower/upper10 becomes a/A
dDecimal integerDefault for ints
fFixed-point floatDefault 6 decimals
.NfN decimal places.2f gives 2 places
e/EScientific notation6.022e+23
%PercentageMultiplies by 100, adds %
,Comma separator1,234,567
_Underscore separator1_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_567
Hints

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

#5Build a Table FormatterMedium
f-stringalignmenttableformatting

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.

Python
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:

  1. Define column widths up front — here: name=15, age=5, gap=4, salary=12, gap=1, city=variable
  2. Left-align text (<) and right-align numbers (>) — this is how humans expect to scan tabular data
  3. Format currency before aligning — build $120,000 first, 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
  4. 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     Boston
Hints

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.

#6Implement __format__ on a Custom ClassMedium
__format__dundercustom-classformat-spec

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".

Python
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:

  1. When Python encounters f"{obj:spec}", it calls obj.__format__(spec) where spec is the string after the colon
  2. The spec is just a string — you parse it however you want. Here we split it into a precision part and a unit character
  3. 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/e type codes convention, adding c and k

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°C
Hints

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.

#7Redirect Print Output to a StringMedium
io.StringIOredirectstdouttesting

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.

Python
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': True
Hints

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.

#8Format Numbers: Currency, Percentage, ScientificMedium
format-speccurrencypercentagescientific-notation

Format numbers for a financial report. Currency should have $ and commas, negative amounts in parentheses. Percentages and scientific notation must have controlled precision.

Python
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.425 becomes 42.50%, 12.5 becomes 1,250.00%
  • Scientific: :.3e gives 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-07
Hints

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.

#9Build a Progress Bar with Carriage ReturnMedium
progress-barcarriage-returnendflush

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.

Python
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:

  • \r moves the cursor to the beginning of the current line without creating a new line
  • The next print() or write() overwrites whatever was on that line
  • end="" or end="\r" prevents print() from adding a newline, keeping the cursor on the same line
  • flush=True (or sys.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, \r does not work well — use IPython.display.clear_output() or tqdm.notebook instead
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

#10Pretty-Print Nested Data StructuresHard
pretty-printrecursionnested-dataformatting

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).

Python
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:

  1. Type dispatch order matters: bool must be checked before int because isinstance(True, int) is True in Python — booleans are a subclass of int
  2. Recursive indentation: Each level increases the indent counter. The closing bracket uses the parent level's indent, creating the cascading visual structure
  3. Comma placement: Commas go after every item except the last. We track position with enumerate and compare against len(items) - 1
  4. 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.

#11Build a Custom Format Spec ParserHard
format-specparsingcustom-formatter__format__

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.

Python
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.

#12Build a Logging-Style Output FormatterHard
loggingtimestampsformattingio.StringIOcallable

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.

Python
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:

  1. 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
  2. Level filtering via numeric values — DEBUG=10 < INFO=20 < WARNING=30 < ERROR=40 < CRITICAL=50. Filtering to WARNING+ means keep entries where level value is 30 or higher.

  3. 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 LogRecord objects instead of plain dicts
  • Formats via Formatter class 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=/health
Hints

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.

© 2026 EngineersOfAI. All rights reserved.