PCAP study materials
PCAP study resources

The String Format Fiasco

PCAP · Put your knowledge to work in context.

All case studies

The String Format Fiasco

You are debugging a function that formats a report line:

def format_line(name, score):
    return f"{name:>10} {score:03d}"

When called with format_line("Alice", 95), it returns ' Alice 095'. When called with format_line("Bob", 7), it returns ' Bob 007'. Now you need to handle a case where score can be a float, e.g., 95.5. You try format_line("Alice", 95.5) and get ValueError: Unknown format code 'd' for object of type 'float'. You decide to change the format spec to {score:03.1f}. Now format_line("Alice", 95.5) returns ' Alice 95.5'. But when you call format_line("Bob", 7), it returns ' Bob 7.0' instead of ' Bob 007.0' or similar. You want the integer part to be zero-padded to at least 3 digits, and the decimal part to always show one digit. So you want 7 to become 007.0 and 95.5 to become 095.5. You try {score:07.1f} and get ' Alice 0095.5' for 95.5 and ' Bob 0007.0' for 7. That's not right either.

1. Explain why `{score:03d}` fails for floats and why `{score:03.1f}` does not zero-pad the integer part as desired. What is the correct format spec to achieve zero-padding to a total width of 7 characters (including the decimal point and one decimal digit) for both integers and floats?

Read the worked answer

The format spec 03d is for integers only; the d presentation type requires an integer, so passing a float raises ValueError. The spec 03.1f means: minimum width 3, precision 1, fixed-point. The width includes the entire formatted string, including the decimal point and digits. For 7, 03.1f produces '7.0' (length 3, no padding needed). For 95.5, it produces '95.5' (length 4, exceeds width 3, so no padding). To zero-pad the integer part to 3 digits while keeping one decimal, you need a total width that accommodates the sign, integer digits, decimal point, and fractional digits. For a positive number with up to 3 integer digits and 1 fractional digit, the total width is 3 + 1 + 1 = 5. But you want the integer part to be at least 3 digits, so for 7 you want 007.0 (length 5). For 95.5 you want 095.5 (length 5). So the width should be 5, and you want zero-padding. The spec 05.1f gives: for 7, '007.0'; for 95.5, '095.5'. That works. However, if the number can have more than 3 integer digits, the width will be exceeded and no padding will occur. If you want to ensure at least 3 integer digits regardless of the fractional part, you can use {score:05.1f}. But note that 05.1f pads with zeros to the left of the entire number, including the sign. For negative numbers, it would produce -07.0? Actually, -7 with 05.1f gives '-7.0' (length 4, padded to 5 with a zero after the sign? Let's check: format(-7, '05.1f') yields '-7.0'? No, it yields '-7.0' with a leading zero? Actually, Python's zero-padding puts the zeros after the sign: -007.0 for width 6? For width 5, -7.0 has length 4, so it pads to 5 with one zero after the sign: -07.0. So it's -07.0. That might be acceptable. The key is to set the width to 5 and use f with precision 1. So the correct spec is 05.1f.

2. If you want to handle both integers and floats with the same format spec, and you want the output to always have exactly 3 integer digits (zero-padded if necessary) and exactly 1 decimal digit, what format spec would you use? What are the limitations?

Read the worked answer

To always have exactly 3 integer digits and 1 decimal digit, you need a total width of 3 + 1 + 1 = 5, and you must ensure the integer part is zero-padded to 3 digits. The format spec 05.1f achieves this for numbers with up to 3 integer digits: for 7, it gives 007.0; for 95.5, it gives 095.5. However, if the number has more than 3 integer digits, e.g., 1234.5, 05.1f will produce 1234.5 (length 6), which exceeds the width and does not truncate. So it does not enforce exactly 3 integer digits; it only ensures a minimum width of 5. To enforce exactly 3 integer digits, you would need to truncate or raise an error for numbers outside the range 0-999.9. Also, for negative numbers, the sign counts toward the width, so -7 with 05.1f gives -07.0 (length 5), which has only 2 integer digits (0 and 7) plus the sign. If you want exactly 3 integer digits regardless of sign, you would need a different approach. So the limitation is that the format spec cannot enforce a fixed number of integer digits; it only sets a minimum total width. You could use a custom function to split the number and format the integer part separately.