Formatting Strings (format, sprintf)

Published by Programming Line in Ruby. Last updated .

Ruby gives you several ways to turn data into precisely formatted text: format, its alias sprintf, the direct-to-stdout printf, and the String#% operator. All four share the same underlying formatting engine and directive syntax borrowed from C’s printf, extended with Ruby-specific features like named references. Once you understand the directive syntax — flags, width, precision, and type — you can control padding, decimal places, number bases, and sign display with one compact string instead of chaining interpolation and manual padding logic together. This lesson covers the full syntax, walks through how the formatting engine processes a directive step by step, and covers the mistakes that trip people up.

Overview: How String Formatting Works in Ruby

format and sprintf are the exact same method under the hood — sprintf is simply an alias for Kernel#format, so you’ll see both names used interchangeably. Both take a format string containing literal text mixed with directives (each starting with %), plus a list of values to substitute into those directives, and they return a brand-new formatted String. printf uses the identical directive syntax but writes the result straight to $stdout (or another IO you pass it) instead of returning a string. The String#% operator is a third way to reach the same engine: "Hello, %s" % "World" calls the formatter internally, using either a single value or an array of values on the right-hand side.

format vs. sprintf vs. printf vs. String#%

Call Returns Typical use
format(fmt, *args) A new String Build a formatted string to store, return, or pass along
sprintf(fmt, *args) A new String Identical to format — just a different name for the same method
printf(fmt, *args) nil (writes to $stdout) Print formatted output directly, without an intermediate string
fmt % args A new String A terse operator form; args is a single value or an array

Under the hood, Ruby scans the format string left to right. Plain characters are copied straight to the output. Whenever it hits a %, it interprets everything up to the next type character (s, d, f, and so on) as instructions for the next argument: optional flags controlling sign and padding style, an optional width that pads the value to a minimum size, and an optional precision that limits decimal places or string length. Because nearly everything in Ruby is an object, the type character determines what conversion happens behind the scenes — %s effectively calls to_s on the argument, so passing nil renders as an empty string (since nil.to_s is ""), not the word “nil”.

Ruby’s format strings also support two features C’s printf doesn’t have: named references (%<name>s or %{name}, pulling values from a trailing keyword hash) and positional references (%1$s, letting you reuse or reorder arguments). Both are covered in the examples below.

Syntax

A directive follows this general shape:

%[flags][width][.precision]type
%[flags][width][.precision]<name>type
%{name}

The first form consumes the next positional argument. The second form pulls a value from a trailing keyword hash by name, while still allowing flags, width, and precision. The third form (%{name}) is a shorthand for a named string substitution with no formatting options.

Directive Meaning Example
%s String (calls to_s) format("%s", 42) → "42"
%d Integer (truncates floats, does not round) format("%d", 3.9) → "3"
%f Fixed-point float (default 6 decimal places) format("%f", 1.5) → "1.500000"
%e Scientific notation format("%e", 12345.6) → "1.234560e+04"
%x / %X Hexadecimal (lower/upper case) format("%x", 255) → "ff"
%o Octal format("%o", 8) → "10"
%b Binary format("%b", 5) → "101"
%c Single character from an Integer codepoint or one-character String format("%c", 65) → "A"
%% A literal percent sign (consumes no argument) format("%d%%", 5) → "5%"
Flag Effect
- Left-justify within the given width (default is right-justify)
0 Pad with leading zeros instead of spaces
+ Always show a sign (+ or -) for numbers
(space) Prefix positive numbers with a space instead of a sign
# Alternate form, e.g. a 0x/0 prefix for %#x/%#o
  • width — the minimum number of characters in the output; the value is padded (with spaces, or zeros if the 0 flag is set) to reach it. It never truncates a value that’s already longer.
  • precision — for %f, the number of digits after the decimal point (default 6); for %s, the maximum number of characters taken from the string; for integer directives, the minimum number of digits, zero-padded.

Examples

Example 1: The basics — format, sprintf, and the % operator

name = "Ada"
age = 36
pi = 3.14159265

puts format("Name: %s, Age: %d", name, age)
puts sprintf("Pi rounded: %.2f", pi)
puts "%-10s|%10s" % ["left", "right"]

Output:

Name: Ada, Age: 36
Pi rounded: 3.14
left      |     right

The first line substitutes a string and an integer directly. The second uses sprintf (identical to format) with a .2 precision to round the float to two decimal places. The third uses the % operator with an array: %-10s left-justifies "left" in a 10-character field, while plain %10s right-justifies "right" in a 10-character field — note the width never truncates, it only pads.

Example 2: Number bases, zero-padding, and sign flags

def show_number_formats(n)
  puts format("Decimal: %d", n)
  puts format("Zero-padded: %05d", n)
  puts format("With sign: %+d", n)
  puts format("Hex: %x  Octal: %o  Binary: %b", n, n, n)
end

show_number_formats(42)

Output:

Decimal: 42
Zero-padded: 00042
With sign: +42
Hex: 2a  Octal: 52  Binary: 101010

%05d pads 42 out to 5 total characters using leading zeros (the 0 flag). %+d forces the + sign that positive numbers normally omit. The last line shows the same integer rendered in three different bases — hex, octal, and binary — each consuming its own copy of n from the argument list.

Example 3: Named references for a realistic invoice line

def invoice_line(item:, qty:, price:)
  format("%-10<item>s x%3<qty>d  $%7.2<price>f", item: item, qty: qty, price: price)
end

puts invoice_line(item: "Widget", qty: 4, price: 19.5)
puts invoice_line(item: "Gadget", qty: 12, price: 5.0)

Output:

Widget     x  4  $  19.50
Gadget     x 12  $   5.00

Instead of positional arguments, each directive pulls its value from the keyword hash by name: %-10<item>s means “left-justify the value called item in a 10-character field as a string,” and %7.2<price>f means “format the value called price as a float with 2 decimal places, right-justified in a 7-character field.” Named references make a long format string self-documenting — you don’t have to count commas to know which value fills which slot.

How It Works Step by Step

Consider format("%08.2f", 3.14159). Ruby parses this directive left to right:

  1. See % — a directive begins.
  2. Read flags: 0 means “pad with zeros.”
  3. Read width: 8 means the final field must be at least 8 characters.
  4. Read precision: .2 means keep exactly 2 digits after the decimal point.
  5. Read the type character: f means “format as a fixed-point float.”
  6. Pull the next argument (3.14159) and round it to the requested precision, giving "3.14".
  7. Since "3.14" is only 4 characters and the width is 8, pad with 4 leading zeros: "00003.14".

Positional references let you reuse or reorder the same arguments by index (%1$ means “the first argument,” %2$ means “the second,” and so on) — handy for message templates where a name needs to appear more than once:

value = 3.14159
puts format("%08.2f", value)

greeting = format("%1$s says hello to %2$s. %1$s waves!", "Ada", "Grace")
puts greeting

Output:

00003.14
Ada says hello to Grace. Ada waves!

Notice "Ada" (argument 1) is reused twice without being passed to format a second time — the index, not the argument list order, controls which value fills each directive.

Common Mistakes

Mistake 1: Assuming %d rounds a float

%d converts its argument to an integer by truncating — it chops off the decimal part, it does not round to the nearest whole number. This silently loses a cent in money calculations:

price = 19.99

puts format("Wrong: $%d", price)
puts format("Right: $%.2f", price)
puts format("Also right (rounded to whole dollars): $%d", price.round)

Output:

Wrong: $19
Right: $19.99
Also right (rounded to whole dollars): $20

%d on 19.99 truncates to 19 — looks like a rounding bug because it drops the .99 instead of rounding up. If you want cents, use %.2f to keep the decimal precision. If you genuinely want a whole-dollar integer, call .round on the value yourself before formatting, so the rounding is explicit rather than an accidental side effect of truncation.

Mistake 2: Forgetting to escape a literal percent sign

Because % always starts a directive, a stray % meant as a literal percent sign confuses the parser:

discount = 20
puts format("Save %d% today!", discount)

Output:

(No output — Ruby raises ArgumentError, because the second % is followed by a space instead of a valid type character, so the parser can't find a directive to close.)

The fix is to double the literal percent sign as %%, which the formatter treats as an escaped literal and does not consume an argument for:

discount = 20
puts format("Save %d%% today!", discount)

Output:

Save 20% today!

Mistake 3: Mixing unnumbered and named references

Ruby doesn’t allow a single format string to mix plain positional directives (%s) with named directives (%<name>s) — you must pick one style consistently:

format("%s and %<name>s", "a", name: "b")

Output:

(No output — Ruby raises ArgumentError, because unnumbered %s references can't be combined with named %<name>s references in the same format string.)

Use named references throughout instead:

puts format("%<first>s and %<second>s", first: "a", second: "b")

Output:

a and b

Best Practices

  • Use format/sprintf to build a string, and printf only when you’re writing straight to stdout and don’t need the string itself.
  • Prefer named references (%<name>s) once a format string has more than two or three placeholders — it makes the call self-documenting and avoids off-by-one positional mistakes.
  • Always give floats an explicit precision (%.2f), especially for money — the default %f precision is 6 decimal places, which is rarely what you want.
  • Remember %d truncates rather than rounds; call .round explicitly first if rounding is what you actually want.
  • Escape every literal percent sign in a format string as %% — a bare % followed by non-directive text raises ArgumentError.
  • Don’t reach for format when plain string interpolation ("#{value}") is enough — save it for cases that actually need padding, precision, or number-base conversion.
  • Let ArgumentError/TypeError from a malformed format call surface rather than rescuing it blindly — it almost always means the format string and the arguments have drifted out of sync.

Practice Exercises

1. Write a method format_price(amount) that formats a Float as US currency with exactly two decimal places and a leading dollar sign, e.g. format_price(7) should return "$7.00" and format_price(1234.5) should return "$1234.50".

2. Using named references, write a method that takes keyword arguments city: and celsius: and returns a string like "Berlin: 18.5°C", with the temperature always shown to one decimal place.

3. Given a fraction like 0.4567, format it as a percentage string with one decimal place, e.g. "45.7%". Hint: multiply by 100 before formatting, and remember to escape the literal percent sign in the format string.

Summary

  • format and sprintf are aliases that return a formatted String; printf writes the same output straight to stdout; String#% is a terse operator form of the same engine.
  • A directive has the shape %[flags][width][.precision]type; flags like -, 0, and + control justification, zero-padding, and sign display.
  • %s converts via to_s, %d truncates floats (it does not round), and %f defaults to 6 decimal places unless you specify a precision.
  • Named references (%<name>s, %{name}) pull values from a keyword hash by name; positional references (%1$s) let you reuse or reorder arguments by index.
  • A literal % in a format string must be escaped as %%, and named references can’t be mixed with unnumbered positional ones in the same call.
  • Malformed format calls raise loud errors (ArgumentError, TypeError) rather than failing silently — treat that as a feature, not an obstacle.