Learning Goals
3 minBy the end of this lesson you can:
- Store each block letter as a list of five strings — the five rows of its shape.
- Look up a letter's shape from a parent list using its alphabetical index (
A→ 0,B→ 1...). - Print a whole word row-by-row by building each row from each letter's row, then printing all five rows in order.
Warm-Up
5 minTake the letter A. If we draw it on a 5-row grid using # and spaces, it looks like this:
### # # ##### # # # #
Five rows. In Python, that's a list of five strings:
A = [ " ### ", "# #", "#####", "# #", "# #", ]
If you print the list, you get a Python list — ugly. To print the actual shape, loop the rows:
for row in A: print(row)
### # # ##### # # # #
That's the whole idea. Today we extend this to many letters and learn the trick that prints a whole word.
Five rows is the smallest height where every English capital is recognisable. Three is too short for E/F/H; six is overkill for one-hour work. Five is the classic banner-font choice.
New Concept · Words Are Built Row by Row
14 minHow banners work: print all top rows first
If you want to print "HI", you can't print the H, then the I — that would put them on top of each other. You print the top row of H + top row of I on line 1, then second row of H + second row of I on line 2, all the way down. Five lines total, side-by-side letters in each.
# # ### # # # ##### # # # # # # ###
One list per letter, all kept together
If we only need uppercase A–Z, we can store all 26 shapes in a single big list, in alphabetical order. The i-th item is the shape of the i-th letter:
LETTERS = [ # index 0 — A [" ### ", "# #", "#####", "# #", "# #"], # index 1 — B ["#### ", "# #", "#### ", "# #", "#### "], # index 2 — C [" ####", "# ", "# ", "# ", " ####"], # ... and so on through Z (index 25) ]
A list of lists. The outer list is "all letters". Each inner list is the five-row shape of one letter.
Looking up a letter from a character
How do we go from the character "C" to index 2? Python has a tiny built-in: ord() returns the unicode number of a character. ord("A") is 65, ord("B") is 66, ord("C") is 67. So:
def index_of(ch): return ord(ch.upper()) - ord("A") print(index_of("A")) # 0 print(index_of("B")) # 1 print(index_of("c")) # 2 (we upper-cased it first)
ord(ch.upper()) - ord("A") reads as "how far is this letter from A". A is 0, B is 1, C is 2. Lower-case is upper-cased first so the lookup works either way.
Print one row of a whole word
Each row of the banner is built by sticking the same-row of each letter side-by-side:
word = "HI" # print row 0 of every letter line = "" for ch in word: line = line + LETTERS[index_of(ch)][0] + " " print(line)
Read that carefully. LETTERS[index_of(ch)] is the list of 5 rows for the letter ch. [0] at the end picks row 0 — the top of that letter. We add a space between letters so they don't touch.
Print all five rows
Wrap the "print one row" trick in an outer loop over the 5 row indexes:
for r in range(5): line = "" for ch in word: line = line + LETTERS[index_of(ch)][r] + " " print(line)
Outer loop walks rows 0 to 4. Inner loop walks the letters of the word. Each iteration of the inner loop adds one letter's share of that row to line. After the inner loop finishes, we print the assembled row and move on to the next.
Anything that prints across rows in a terminal needs to be built row by row, in order. You can't print a column-of-text top-to-bottom and then move to the next column — the terminal doesn't move backwards.
Worked Example · Type Your Name, See Your Name
15 minA 5-letter alphabet — enough to spell "HELLO"
Twenty-six letters is a lot to type. For class we'll start with the ones we need to spell "HELLO" — H, E, L, O. Plus a space, so multi-word names work. Save as banner.py:
Code
# banner.py — ASCII banner generator (mini alphabet) # 5-row shapes. Each letter is a list of 5 strings. LETTERS = { "A": [" ### ", "# #", "#####", "# #", "# #"], "E": ["#####", "# ", "#### ", "# ", "#####"], "H": ["# #", "# #", "#####", "# #", "# #"], "I": ["#####", " # ", " # ", " # ", "#####"], "L": ["# ", "# ", "# ", "# ", "#####"], "O": [" ### ", "# #", "# #", "# #", " ### "], " ": [" ", " ", " ", " ", " "], } def print_banner(text): text = text.upper() for r in range(5): line = "" for ch in text: if ch in LETTERS: line = line + LETTERS[ch][r] + " " else: line = line + "?" * 5 + " " print(line) # main program name = input("Type your name: ").strip() print() print_banner(name)
Why a dictionary, not a list?
Look at LETTERS — it's wrapped in { and } with "key": value pairs. That's a dictionary — a lookup by key, not by position. We'll meet dictionaries properly in Level 2, but for today: LETTERS["H"] gives back the H shape, no ord arithmetic needed. Cleaner for a sparse alphabet.
If a character isn't in the dictionary, we print ????? as a placeholder. That's the else branch.
Sample run
Type your name: hello # # ##### # # ### # # # # # # # ##### #### # # # # # # # # # # # # # ##### ##### ##### ###
Walking the function in your head
Trace what happens for the word "HI":
text.upper()→"HI".- Outer loop:
r = 0. Inner loop:ch = "H"→line = "# # ";ch = "I"→line = "# # ##### ". Print. ✓ - Outer loop:
r = 1. Inner loop:ch = "H"→line = "# # ";ch = "I"→line = "# # # ". Print. ✓ - ...and so on through
r = 4.
The hard work happens in the data (the letter shapes) — the function is short and stays the same forever. To add the letter X, you add one row to LETTERS; the function doesn't care. That's the data-driven design we've been preaching for ten lessons, finally paying off in pixels.
Try It Yourself
13 minThree tasks. Build the small version first, then add letters and polish.
Make a list A with the five rows of the letter A. Print them one per line by looping. Then try B = [...] and print B.
Hint
A = [" ### ", "# #", "#####", "# #", "# #"] for row in A: print(row)
If you don't see anything, double-check the list has commas between every string. Five strings, five commas (including the optional trailing one), five rows.
Recreate banner.py from the worked example. Run it. Try typing HELLO, HI, HOLA. The H, E, L, O letters should appear correctly. Try typing HX — what shows up?
Hint
For HX you should see the H drawn correctly, then five ? characters in each row where the X would have been. That's the placeholder for "letter not in dictionary" behaviour, exactly what we coded.
Add at least two more letters to LETTERS so you can spell your own name (or part of it). Draw each one on paper first on a 5×5 grid of squares; type the rows in. Test.
Hint · here's the letter N
"N": ["# #", "## #", "# # #", "# ##", "# #"],
Tip: draw the letter on graph paper first. Each square is either # or space. Then translate row by row. Don't skip the paper step — eyeballing pixel art straight in code is a recipe for crooked letters.
Mini-Challenge · Iqbal's Stacked Banner
8 minIqbal's banner prints all five rows of H, then all five rows of I — stacked vertically instead of side-by-side. Find the bug.
# iqbal_banner.py — buggy
def print_banner(text):
text = text.upper()
for ch in text: # outer loop is over letters
for r in range(5): # inner loop is over rows
print(LETTERS[ch][r])
print_banner("HI")What does this print? The 5 rows of H, then the 5 rows of I, one above the other. Why? Because the loops are nested the wrong way around — for each letter, we're printing all its rows before moving to the next letter.
The fix: swap the loop order. Outer loop over rows, inner loop over letters. And don't print inside the inner loop — accumulate into a line string, then print once per row.
Show the fix
def print_banner(text): text = text.upper() for r in range(5): # outer = rows line = "" for ch in text: # inner = letters line = line + LETTERS[ch][r] + " " print(line)
The bug is the most common 2D-printing mistake: thinking in objects ("print each letter") instead of in rows ("print row 0 of all letters, then row 1, ..."). Terminals move strictly left-to-right, top-to-bottom — your loops must respect that order.
Recap
3 minA banner generator boils down to three ideas. (1) Store each letter as a list of five strings — its shape. (2) Keep all the letters in a dictionary keyed by character, so LETTERS["H"] gives the shape directly. (3) To draw a word, walk rows on the outside and letters on the inside — print row 0 of every letter first, then row 1, and so on. The function stays short because the data carries the weight. Tonight you'll grow the alphabet so it spells your full name; next lesson we leave block letters behind and start the Text Adventure series.
Vocabulary Card
- list of strings
- A list whose items are strings. Perfect for storing the rows of a multi-line shape.
- dictionary (preview)
- A lookup-by-key collection:
{"H": [...], "I": [...]}. Full deep-dive in Level 2 (PY-L2-05). ord(ch)- Built-in that returns the Unicode code point of a character.
ord("A")is 65. - row-major printing
- The standard way to print 2D content: outer loop on rows, inner loop on columns.
- line accumulator
- A string variable built up inside an inner loop (
line = line + ...) and printed once per outer iteration.
Homework
4 minPolish banner.py into something you'd show off.
- Add every letter you need to spell your full name. Save the file with your name as a comment at the top:
# banner.py — banner for "Aisyah Rahman". - Wrap the program in a
while Trueloop so the user can type name after name without restarting. Break on"quit". - Add a top and bottom border line of
=equal signs around the banner — the same width as the printed banner if you can manage it. (Hint: each letter is 5 chars wide + 1 space.) - Bonus: let the user choose the "ink" character at the start — by default
#, but they can type*or@. Use.replace("#", ink)on each row before printing.
Bring banner.py next class — PY-L1-39 starts the three-part Text Adventure.
Sample · banner.py with loop & ink colour
# banner.py — banner for "Aisyah Rahman" LETTERS = { "A": [" ### ", "# #", "#####", "# #", "# #"], "H": ["# #", "# #", "#####", "# #", "# #"], "I": ["#####", " # ", " # ", " # ", "#####"], "M": ["# #", "## ##", "# # #", "# #", "# #"], "N": ["# #", "## #", "# # #", "# ##", "# #"], "R": ["#### ", "# #", "#### ", "# # ", "# #"], "S": [" ####", "# ", " ### ", " #", "#### "], "Y": ["# #", " # # ", " # ", " # ", " # "], " ": [" ", " ", " ", " ", " "], } def print_banner(text, ink): text = text.upper() width = 6 * len(text) print("=" * width) for r in range(5): line = "" for ch in text: shape = LETTERS.get(ch, ["?????"] * 5) line = line + shape[r].replace("#", ink) + " " print(line) print("=" * width) while True: name = input("Name (or 'quit'): ").strip() if name.lower() == "quit": break ink = input("Ink character [#]: ").strip() or "#" print() print_banner(name, ink) print()
Two slick moves: LETTERS.get(ch, ["?????"] * 5) is the dictionary version of "in-or-default" — no if ch in LETTERS needed. And input(...) or "#" uses the "empty string is falsy" trick — if the user just hits Enter, the default "#" is used. Both come back later in real programs all the time.