Recipes
Practical, copy-paste examples demonstrating common Craftable patterns and advanced techniques.
Color-code negative values with ANSI escape codes
Use a postprocessor to add red coloring to negative numbers:
from craftable import get_table
def red_negative(original, text, row, col_idx):
try:
if float(original) < 0:
return f"\x1b[31m{text}\x1b[0m" # red
except (ValueError, TypeError):
pass
return text
data = [
["Widget A", 1250.50, -45.20],
["Widget B", -320.00, 150.75],
["Widget C", 890.25, 220.00],
]
print(get_table(
data,
header_row=["Product", "Revenue", "Cost"],
col_defs=["<20", ">10.2f", ">10.2f"],
postprocessors=[None, red_negative, red_negative],
))
Example output (colors removed for clarity):
Product │ Revenue │ Cost
──────────────────────┼────────────┼────────────
Widget A │ 1250.50 │ -45.20
Widget B │ -320.00 │ 150.75
Widget C │ 890.25 │ 220.00
Format currency with prefix and conditional coloring
Combine prefix formatting with postprocessors for currency display:
from craftable import get_table
def color_by_value(original, text, row, col_idx):
try:
val = float(original)
if val < 0:
return f"\x1b[31m{text}\x1b[0m" # red
elif val > 1000:
return f"\x1b[32m{text}\x1b[0m" # green
except (ValueError, TypeError):
pass
return text
transactions = [
["Groceries", -152.43],
["Paycheck", 2500.00],
["Utilities", -89.12],
["Bonus", 1500.00],
]
print(get_table(
transactions,
header_row=["Description", "Amount"],
col_defs=["", "<$ (>10.2f)"],
postprocessors=[None, color_by_value],
))
Example output (colors removed for clarity):
Description │ Amount
─────────────┼────────────
Groceries │ $ -152.43
Paycheck │ $ 2500.00
Utilities │ $ -89.12
Bonus │ $ 1500.00
Render an accounting ledger
Use a post-processor to transform standard currency into the specialized accounting format.
from datetime import date
import re
from craftable import get_table
# Define the format definition as well as a regular expression pattern to select
# only the number and sign. Replace symbols, separators, and decimal as needed.
accounting_def = f"<$ (>,.2f) "
# separator v v decimal
padded_num = re.compile(r'[- ]?(\d{1,3}(\,\d{3})*\.\d{2}) ')
def accounting_format(original, text, row, col_idx):
if original <= 0:
print(text)
match = re.search(padded_num, text)
print(match)
if match:
full_text = match.group(0)
num_part = match.group(1)
if original < 0:
new_text = f"({num_part})"
else:
new_text = (" " * (len(full_text) - 2)) + "- "
return re.sub(padded_num, new_text, text)
return text
data = [
[ date(2025, 6, 15), "Lowe's", -54.25, 98.23 ],
[ date(2025, 6, 17), "Walmart", -62.83, 35.4 ],
[ date(2025, 6, 17), "Petsmart", -35.4, 0 ],
[ date(2025, 6, 18), "Deposit", 1500, 1500 ],
]
print(get_table(
data,
header_row=["Date", "Description", "Amount", "Balance"],
col_defs=["", "", accounting_def, accounting_def],
postprocessors=[None, None, accounting_format, accounting_format],
))
Example output:
Date │ Description │ Amount │ Balance
────────────┼─────────────┼─────────────┼─────────────
2025-06-15 │ Lowe's │ $ (54.25) │ $ 98.23
2025-06-17 │ Walmart │ $ (62.83) │ $ 35.40
2025-06-17 │ Petsmart │ $ (35.40) │ $ -
2025-06-18 │ Deposit │ $ 1,500.00 │ $ 1,500.00
Build a progress/status indicator table
Use Unicode characters and postprocessors for visual status:
from craftable import get_table
def status_icon(original, text, row, col_idx):
status_map = {
"done": "✓",
"pending": "⧗",
"failed": "✗",
}
return status_map.get(original, text)
tasks = [
["Deploy to prod", "done"],
["Run tests", "done"],
["Update docs", "pending"],
["Code review", "failed"],
]
print(get_table(
tasks,
header_row=["Task", "Status"],
col_defs=["", "^8"],
postprocessors=[None, status_icon],
))
Example output:
Task │ Status
────────────────┼──────────
Deploy to prod │ ✓
Run tests │ ✓
Update docs │ ⧗
Code review │ ✗
Convert dates with preprocessor, color with postprocessor
Chain preprocessing (for formatting) and postprocessing (for decoration):
from craftable import get_table
from datetime import date, timedelta
def format_date(val, row, col_idx):
if isinstance(val, date):
return val.strftime("%Y-%m-%d")
return val
def highlight_recent(original, text, row, col_idx):
if isinstance(original, date):
days_ago = (date.today() - original).days
if days_ago < 7:
return f"\x1b[1m{text}\x1b[0m" # bold
return text
events = [
["Meeting", date.today() - timedelta(days=2)],
["Launch", date.today() - timedelta(days=30)],
["Review", date.today()],
]
print(get_table(
events,
header_row=["Event", "Date"],
col_defs=["<20", "<12"],
preprocessors=[None, format_date],
postprocessors=[None, highlight_recent],
))
Example output (bold removed for clarity):
Event │ Date
──────────────────────┼──────────────
Meeting │ 2025-11-13
Launch │ 2025-10-16
Review │ 2025-11-15
Parse fuzzy date strings with dateparser
Use the dateparser library to parse various date string formats and format them consistently with a preprocessor:
from craftable import get_table
# Various date formats that dateparser can handle
appointments = [
["Team standup", "tomorrow at 9am"],
["Client demo", "next Friday"],
["Code review", "in 3 days"],
["Release", "2025-12-01"],
["Conference", "Jan 15, 2026"],
]
def parse_date_string(val, row, col_idx):
"""Parse string dates with dateparser and format as YYYY-MM-DD."""
if not isinstance(val, str):
return val
try:
import dateparser
parsed = dateparser.parse(val)
if parsed:
return parsed.strftime("%Y-%m-%d")
except ImportError:
# dateparser not available, return original
pass
except Exception:
# Parsing failed, return original
pass
return val
print(get_table(
appointments,
header_row=["Event", "Date"],
col_defs=["<20", "<12"],
preprocessors=[None, parse_date_string],
))
Example output (dates relative to execution time):
Event │ Date
─────────────────────┼──────────────
Team standup │ 2025-11-17
Client demo │ 2025-11-21
Code review │ 2025-11-19
Release │ 2025-12-01
Conference │ 2026-01-15
Note: Requires the dateparser package: pip install dateparser
Generate a multi-level grouped summary
Build hierarchical data with visual indentation:
from craftable import get_table
def indent_category(value, row, col_idx):
if isinstance(value, str) and value.startswith(" "):
return f" └─ {value.strip()}"
return value
data = [
["Electronics", None, 5420.00],
[" Laptops", 3, 3200.00],
[" Monitors", 5, 2220.00],
["Furniture", None, 1890.50],
[" Desks", 2, 890.00],
[" Chairs", 4, 1000.50],
]
print(get_table(
data,
header_row=["Category", "Qty", "Total"],
col_defs=["<25", ">5", ">10.2f"],
preprocessors=[indent_category, None, None],
))
Example output:
Category │ Qty │ Total
───────────────────────────┼───────┼────────────
Electronics │ │ 5420.00
└─ Laptops │ 3 │ 3200.00
└─ Monitors │ 5 │ 2220.00
Furniture │ │ 1890.50
└─ Desks │ 2 │ 890.00
└─ Chairs │ 4 │ 1000.50
Build a linked styles overview table (Markdown)
Generate a Markdown table with postprocessor-added links to page anchors:
from itertools import zip_longest
from craftable import get_table
from craftable.styles import MarkdownStyle
display = [
"NoBorderScreenStyle",
"BasicScreenStyle",
"RoundedBorderScreenStyle",
"MarkdownStyle",
"ASCIIStyle",
]
export = [
"XlsxStyle",
"OdsStyle",
"OdtStyle",
"DocxStyle",
"RtfStyle",
]
rows = [list(pair) for pair in zip_longest(display, export)]
anchor_map = {
"NoBorderScreenStyle": "style-noborder",
"BasicScreenStyle": "style-basic",
"RoundedBorderScreenStyle": "style-rounded",
"MarkdownStyle": "style-markdown",
"ASCIIStyle": "style-ascii",
"XlsxStyle": "style-xlsx",
"OdsStyle": "style-ods",
"OdtStyle": "style-odt",
"DocxStyle": "style-docx",
"RtfStyle": "style-rtf",
}
def linkify(original, text, row, col_idx):
if not text.strip():
return text
anchor = anchor_map.get(original, "")
anchor_text = f"[{original}](#{anchor})" if anchor else original
return text.replace(original, anchor_text)
print(get_table(
rows,
header_row=["Display Styles", "Export Styles"],
style=MarkdownStyle(),
postprocessors=[linkify,linkify],
))
Example output (raw Markdown table):
| Display Styles | Export Styles |
| ------------------------ | ------------- |
| [NoBorderScreenStyle](#style-noborder) | [XlsxStyle](#style-xlsx) |
| [BasicScreenStyle](#style-basic) | [OdsStyle](#style-ods) |
| [RoundedBorderScreenStyle](#style-rounded) | [OdtStyle](#style-odt) |
| [MarkdownStyle](#style-markdown) | [DocxStyle](#style-docx) |
| [ASCIIStyle](#style-ascii) | [RtfStyle](#style-rtf) |
See this table rendered on the styles page.
Truncate long log messages for compact output
Use the truncate flag for log-friendly tables:
from craftable import get_table
from craftable.styles import NoBorderScreenStyle
logs = [
["2025-01-15 10:23:45", "INFO", "Application started successfully"],
["2025-01-15 10:24:12", "WARN", "Database connection pool is running low on available connections"],
["2025-01-15 10:25:03", "ERROR", "Failed to process request: timeout exceeded after 30 seconds"],
]
print(get_table(
logs,
header_row=["Timestamp", "Level", "Message"],
col_defs=["<19", "<6", "40T"], # T flag = truncate with ellipsis
style=NoBorderScreenStyle(),
))
Example output:
Timestamp │ Level │ Message
─────────────────────┼────────┼───────────────────────────────────────────
2025-01-15 10:23:45 │ INFO │ Application started successfully
2025-01-15 10:24:12 │ WARN │ Database connection pool is running low…
2025-01-15 10:25:03 │ ERROR │ Failed to process request: timeout exce…
Long log messages for without wrapping
Use an appropriate style and set lazy_end=True:
from craftable import get_table
from craftable.styles import ASCIIStyle
logs = [
["2025-01-15 10:23:45", "INFO", "Application started successfully"],
["2025-01-15 10:24:12", "WARN", "Database connection pool is running low on available connections"],
["2025-01-15 10:25:03", "ERROR", "Failed to process request: timeout exceeded after 30 seconds"],
]
print(get_table(
logs,
header_row=["Timestamp", "Level", "Message"],
style=ASCIIStyle(),
lazy_end=True,
))
Example output:
+---------------------+-------+-------------------------------------------------------------------
| Timestamp | Level | Message
+---------------------+-------+-------------------------------------------------------------------
| 2025-01-15 10:23:45 | INFO | Application started successfully
| 2025-01-15 10:24:12 | WARN | Database connection pool is running low on available connections
| 2025-01-15 10:25:03 | ERROR | Failed to process request: timeout exceeded after 30 seconds
+---------------------+-------+-------------------------------------------------------------------
Pivot data from columnar to row format
Transform a mapping-of-lists into a transposed table:
from craftable import get_table
from craftable.adapters import from_mapping_of_lists
stats = {
"metric": ["CPU %", "Memory MB", "Disk I/O"],
"server_a": [45.2, 2048, 1250],
"server_b": [78.9, 4096, 3420],
"server_c": [23.1, 1024, 890],
}
rows, headers = from_mapping_of_lists(stats)
print(get_table(rows, header_row=headers, col_defs=["<12", ">10", ">10", ">10"]))
Example output:
metric │ server_a │ server_b │ server_c
──────────────┼────────────┼────────────┼────────────
CPU % │ 45.2 │ 78.9 │ 23.1
Memory MB │ 2048 │ 4096 │ 1024
Disk I/O │ 1250 │ 3420 │ 890
Create a comparison table with trend indicators
Show before/after comparisons with visual markers:
from craftable import get_table
def trend_arrow(val, row, col_idx):
"""Add up/down arrows based on change from prior column."""
trend = " "
try:
prior_val = row[col_idx - 1]
if val > prior_val:
trend = "↑"
elif val < prior_val:
trend = "↓"
except Exception:
pass
return f"{val} {trend}"
# Manually add trend info to data
comparisons = [
["Response time (ms)", "245", "198"],
["Error rate (%)", "2.1", "0.8"],
["Throughput (req/s)", "1200", "1450"],
]
print(
get_table(
comparisons,
header_row=["Metric", "Before", "After"],
col_defs=["<20", ">10", ">10"],
preprocessors=[None, None, trend_arrow],
)
)
Example output:
Metric │ Before │ After
──────────────────────┼────────────┼────────────
Response time (ms) │ 245 │ 198 ↓
Error rate (%) │ 2.1 │ 0.8 ↓
Throughput (req/s) │ 1200 │ 1450 ↑
Display percentages with progress bars
Use Unicode block characters for inline progress visualization:
from craftable import get_table
def progress_bar(val, row, col_idx):
try:
pct = float(val)
bars = int(pct / 10)
return "█" * bars + "░" * (10 - bars) + f" {val}%"
except (ValueError, TypeError):
return val
completion = [
["Backend API", 85],
["Frontend UI", 60],
["Database migrations", 100],
["Documentation", 45],
]
print(get_table(
completion,
header_row=["Component", "Progress"],
preprocessors=[None, progress_bar],
))
Example output:
Component │ Progress
─────────────────────┼─────────────────
Backend API │ ████████░░ 85%
Frontend UI │ ██████░░░░ 60%
Database migrations │ ██████████ 100%
Documentation │ ████░░░░░░ 45%
Read a CSV and select columns
Load a CSV with Python's standard library and build a table using only a subset of columns. This example also formats salary with a left-aligned "$" prefix and formats a percentage column using the normal f-string % format type.
import csv
from craftable import get_table
from craftable.adapters import from_dicts
# employees.csv (example with interleaved extra columns):
# id,name,dept,manager,salary,currency,bonus_pct,notes,location
# 101,Alice,Engineering,Elaine,120000,USD,0.10,Senior engineer,NYC
# 102,Bob,Sales,Marco,85000,USD,0.05,Top performer,Austin
# 103,Carol,Support,Janet,65000,USD,0.07,,Remote
def to_float(val, *_): # note the *_ to accept unused extra args
try:
return float(val)
except (TypeError, ValueError):
return val
with open("employees.csv", newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
# Keep only the columns we care about, in the order we want
desired = ["name", "dept", "salary", "bonus_pct"]
rows, headers = from_dicts(reader, columns=desired)
# Optionally, prettify headers
headers = [h.replace("_", " ").title() for h in headers]
print(get_table(
rows,
header_row=headers,
col_defs=[
"", # name
"", # dept
"<$ (>,.0f)", # salary: left-aligned $ with right-aligned, grouped value
"(>.1%)", # bonus_pct: normal f-string percentage formatting
],
preprocessors=[None, None, to_float, to_float],
))
Example output:
Name │ Dept │ Salary │ bonus_pct
──────────────────────┼─────────────┼───────────────┼──────────
Alice │ Engineering │ $ 120,000│ 10.0%
Bob │ Sales │ $ 85,000│ 5.0%
Carol │ Support │ $ 65,000│ 7.0%
Pandas variant
Do the same selection and formatting using pandas. Pandas automatically coerces numbers when reading files, eliminating the need for the preprocessor callback.
# Requires pandas (optional dependency)
import pandas as pd
from craftable import get_table
from craftable.adapters import from_dataframe
# Same CSV as above (with interleaved extra columns)
df = pd.read_csv("employees.csv")
desired = ["name", "dept", "salary", "bonus_pct"]
rows, headers = from_dataframe(df, columns=desired)
# Optionally, prettify headers
headers = [h.replace("_", " ").title() for h in headers]
print(get_table(
rows,
header_row=headers,
col_defs=[
"", # name
"", # dept
"<$ (>,.0f)", # salary: left-aligned $ with right-aligned, grouped value
"(>.1%)", # bonus_pct: normal f-string percentage formatting
],
))