🖨️ Displaying Output - Advanced
🖨️ Displaying Output - Advanced
The first Displaying Output lesson covered print(), sep, end and f-strings. This lesson is about the next problem: printing collections (lists, tuples, dictionaries, nested combinations of them) so the result is neat rather than a wall of brackets and quotes. The star of the show is str.join(), but it has several companions: unpacking with *, repr, dynamic field widths, pprint and json.dumps. The lesson finishes with a critical look at whether a loop like this belongs inline or in a function. As requested, the functions in this lesson are discussed in words only, and none of the examples define one.
join lives on the separator in Python, not on the collection. JavaScript has arr.join(", "), which only works on arrays. Java has String.join(", ", list), which is much closer to Python's design. In Python you write ", ".join(items), and because it is a string method it accepts any iterable: a list, a tuple, a set, dictionary keys, a generator, even another string. The second difference is strictness. JavaScript's join quietly converts numbers (and turns null into an empty string); Python's raises TypeError unless every item is already a string. The third is that Python has no JSON.stringify on the global scope: the equivalent is json.dumps() from the standard json module.
json.dumps(data, indent=2) is the quickest way to see in the terminal what that response body will look like. pprint is the same idea for a Python-flavoured view (True and None stay as they are, tuples stay tuples). Knowing the two apart saves confusion when a value changes shape on the way out: tuples become JSON arrays, True becomes true and None becomes null.
🧰 The Toolbox
Ten tools, most of them one line long. The first three are about turning a collection into text; the rest are about making that text look good.
str, otherwise TypeError. Convert on the way in with a generator expression, or with map(str, nums).* spreads a collection into separate arguments. print() converts each one to a string itself, so numbers need no str() at all, and sep does the joining.!r (or repr()) shows a value the way you would type it in code, so "7" and 7 stop looking identical. Collections already use it for their items.None.print(). The output looks much like the snippet you started from, but it is real, valid JSON.print() has two more keyword arguments. file chooses where the text goes (the normal output channel, the error channel, or an open file); flush=True pushes it out immediately instead of waiting.% multiplies by 100 and adds the sign, and + forces a sign on positive numbers. They combine freely with width and precision.🔗 What join() Actually Does
Three facts explain almost everything about join.
- The string you call it on is the glue. In
" ~~~ ".join(value)the separator is" ~~~ ", andvalueis the thing being walked through. Python takes the first item, then for each remaining item adds the glue and then the item. The glue goes between items only. That is whyjoinbeats a hand-written loop withend=", ", which always leaves a stray separator after the last item. - It returns a new string and prints nothing.
joinonly builds text, so you hand the result toprint()or place it inside an f-string, exactly as the snippet does with{" ~~~ ".join(value)}. Because it builds the whole string in one go, it is also the efficient way to assemble text from many pieces, rather than+=inside a loop. - Every item must be a string. A list of numbers, booleans or nested lists raises
TypeError. The usual fix is", ".join(str(item) for item in value), or the shorter", ".join(map(str, value)). When the items are strings already, as in the snippet, you can pass the collection straight in.
When to reach for it: whenever you have a collection of pieces and want one line of text from them: a comma-separated list for a sentence or a CSV row, a path built from folder names, a multi-line block ("\n".join(lines)) printed with a single print(), or the contents of a list or tuple dropped into the middle of a label. When not to: if you only want the items on one line and they are not all strings, print(*items, sep=", ") is shorter and needs no conversion; and if the structure is nested, pprint or json.dumps will do the whole job for you.
✳️ The Star Alternative: print(*items)
print() already accepts any number of arguments, joins them with sep and converts each to a string. The * operator turns one collection into that many arguments, which gives you join's result without the string conversion step.
.items() to get (key, value) tuples, and set sep="\n" to put each on its own line.print(*items, sep=...) when you are printing straight away and the items might not be strings. Use join when you need the text as a value: inside an f-string, in a variable, in a file. The star only works inside a call, so it cannot sit inside an f-string's text.
🔬 Dissecting the Snippet
Here is the code from the dictionary-iteration lesson again, piece by piece, followed by what it actually prints for a student record.
| Piece | What it does |
|---|---|
for key, value in student_info.items(): | Walks the dictionary one entry at a time, unpacking each (key, value) pair into two variables. |
isinstance(value, list) | Asks whether this entry's value is a list. The if / elif / else chain then picks a different print statement for each type: list, tuple, dict, and everything else. |
f'"{key}" : ...' | The outer quotes are single, so the double quotes inside are ordinary characters that appear in the output ("name" : ...). Only what is inside { } is evaluated. |
{" ~~~ ".join(value)} | An expression inside the braces: join the list's items with ~~~ between them. It uses double quotes inside a single-quoted f-string on purpose (see the gotchas). |
{" ".join(value)} (tuple branch) | The same idea with a plain space as the glue, so a tuple prints as words separated by spaces. |
| the dict branch | Prints the key on a line of its own, then a second loop over the inner dictionary, indented by two spaces, one line per inner pair. |
the else branch | Strings, numbers, booleans: anything that needs no special treatment prints as "key" : "value". |
the trailing , | A literal comma at the end of every line, imitating how a dictionary looks when written out in code. |
"21", in quotes, because the else branch quotes everything.Where the snippet is fragile
- A list of numbers crashes it. If
"modules"were[101, 205]," ~~~ ".join(value)would raiseTypeError. It works here only because every list and tuple happens to hold strings. - The
elsebranch quotes numbers and booleans.21andTrueprint as"21"and"True", so a number looks like a string. - It goes exactly one level deep. A list inside the inner dictionary would print in raw bracket form, because the inner loop has no type checks of its own. Handling it means copying the whole
if / elifchain inside the inner loop, and then again inside that. - Every line ends with a comma, including the last. It looks like JSON but is not valid JSON.
None of this matters for a classroom exercise. All four are exactly the kind of thing that starts to matter when the code has to cope with other dictionaries, which brings us to the reason for the next two examples and the function discussion after them.
♻️ The Same Pattern, Different Data
The structure is identical (.items(), an isinstance chain, a nested loop) but this version fixes the fragile points: it converts list items with str(), merges lists and tuples into one branch, drops the quotes, and aligns the keys in a column.
{key:<10} pads every key to 10 characters so the values line up. str(item) for item in value lets the list of ratings through join. The single-item tuple is written ("Luciano Ramalho",): the trailing comma is what makes it a tuple.🪄 Letting the Standard Library Do It
If the aim is just to look at a nested structure, the loop above is more work than needed. pprint and json.dumps handle any depth and any mix of types.
pprint keeps Python notation, while json.dumps converts the tuple to a list and, with indent=2, spreads everything out.pprint for debugging (it shows exactly what Python holds). json.dumps when the output is headed for an API, a file or another program. A hand-written loop when you need a specific house style, like the one in the snippet.
📏 Case Study: A Dictionary as an Aligned Table
Dynamic field widths and the extra format specs together: the column width comes from the data itself, so adding a longer name later never breaks the layout.
width is computed once from the longest key, then used inside the format spec as {name:<{width}}. The separator line is sized from the same number.🧩 A Critical Look: Loop Inline, or in a Function?
The idea: instead of repeating the isinstance chain, write a function that receives the data, works out each value's type and prints it appropriately. Functions come later in the course, so this section describes the design in words only. Your instinct is sound, but how sound depends on the situation.
What the function would look like
It is worth being precise about the design, because the version you described has a choice in it. You said you would pass over the key and value, then wondered whether passing the whole dictionary is better. It is, for most purposes. If the caller passes one pair at a time, the caller still has to write the for key, value in ... .items() loop each time; only the type chain has moved. If the caller passes the dictionary, the function owns the loop too, and the calling code collapses to one line per dictionary. Passing a single pair only wins when you want to print a handful of selected entries, or in a different order.
There are two further refinements worth knowing about. A function that returns the text instead of printing it is more flexible, because the caller decides whether to print it, save it to a file or check it. And a function that receives a dictionary can, for a nested dictionary, call itself on the inner one. That is something an inline loop cannot do: it can only handle as many levels as you physically copy the code for.
| For a function | Against a function |
|---|---|
The type-testing chain is written once. Fixing the quote problem or adding a set branch is one edit, not one per place it was pasted. | In a short script that prints one dictionary once, it adds a name, a header and a jump for the reader to follow, for no saving. |
| The call site reads like a sentence (a descriptive name and one dictionary), so the main program is easier to read. | You are designing for dictionaries you have not seen yet. A function written around one example often needs reworking for the second. |
| Different dictionaries need no new code: pass any of them. | The name matters. A vague one ends up being worse for readability than the loop it replaced. |
| It can handle any nesting depth by calling itself, which the inline version cannot do at all. | Python reads a script top to bottom, so a function must be defined before the line that calls it. Java does not care about order within a class, and JavaScript hoists function declarations; Python does neither. |
| It can be tested and debugged in isolation, and moved into its own file and reused across programs. | For plain inspection, the standard library may already be the better function: pprint and json.dumps are exactly this, written and tested by someone else. |
Which is better, and when
| Situation | Better choice | Why |
|---|---|---|
| Small script, one dictionary, printed once | Inline loop | Nothing is repeated, so there is nothing to save. Everything is visible in one place. |
Learning how loops, .items() and isinstance work | Inline loop | The loop is the subject. Hiding it in a function hides the lesson. This is a good reason for your teacher's rule. |
| The same block pasted a second time | Either | A common rule of thumb is to copy once, and turn it into a function the third time. |
| Larger program, many different dictionaries | Function (pass the dictionary) | One definition, many calls, one place to fix bugs. |
| Data of unknown shape, such as an API response | A function that calls itself, or json.dumps | Nesting depth cannot be predicted, so a fixed number of copied loops will eventually be wrong. |
| Just inspecting data while debugging | pprint / json.dumps | Zero code to write, and it handles every type. |
| Code others will read or maintain | Function, well named | The reader learns what it does from the name, and only reads the detail if they need to. |
One more critical point. A growing if / elif chain that tests types is, in Python terms, slightly against the grain: Python usually prefers asking an object to do something rather than asking what it is. For display code that is accepted, because the alternative (making each type print itself) is more machinery than the job deserves. If the chain keeps growing past four or five types, though, that is the sign to stop and ask whether pprint or json.dumps already does it.
📋 Quick Reference — Advanced Output
| Task | Syntax | Notes |
|---|---|---|
| Join strings with a separator | ", ".join(items) | Call it on the separator. All items must be str. |
| Join non-strings | ", ".join(map(str, items)) | Or str(i) for i in items. |
| Print a collection on one line | print(*items, sep=", ") | No conversion step needed; works only as a call. |
| One item per line | print(*items, sep="\n") | Or print("\n".join(items)) for strings. |
| Show the quotes on a value | f"{x!r}" / repr(x) | Tells "7" from 7. |
| Width from a variable | f"{x:<{w}}" | Nested braces inside the spec. |
| Thousands separator / percent | f"{n:,}" / f"{r:.1%}" | Combine with width and precision. |
| Pretty-print nested data | pprint(data) | from pprint import pprint. sort_dicts=False keeps your key order. |
| JSON text | print(json.dumps(data, indent=2)) | Returns a string. sort_keys=True sorts the keys. |
| Send text to the error channel | print(msg, file=sys.stderr) | import sys first. flush=True forces immediate output. |
| Indent by nesting level | " " * depth + text | String repetition. |
["a", "b"].join(", ") raises AttributeError: lists have no join method. The separator is the object: ", ".join(["a", "b"]).Numbers are not converted for you: where JavaScript's
[1, 2].join() returns "1,2", Python's ",".join([1, 2]) raises TypeError. Convert with map(str, ...), or use print(*items).Reusing the outer quote inside an f-string's braces is a
SyntaxError before Python 3.12: f"{", ".join(x)}" only works on 3.12 or later. That is why the snippet wraps the whole f-string in single quotes: it uses double quotes both for the literal text and inside the expression. On any version, choosing different quote types is the safe habit.print(pprint(data)) prints the data and then None: pprint prints on its own and returns nothing. If you want the text as a string, use pprint.pformat(data) instead.json.dumps is not a drop-in for print: it returns a string, turns tuples into lists, writes True as true and None as null, and raises TypeError for values JSON has no form for, such as a set.Printing a collection directly shows
repr of its items: print(["a", "b"]) gives ['a', 'b'] with brackets and quotes. That is correct behaviour; join or print(*...) is how you ask for something different.