CH NEO-ZÜRICH EDITION
WEATHER · OVERCAST 5°C
BLEND OF THE DAY · 07/ROGUE
EST. 2027
THE AEC CYBER MORNING NEWS

PAZ Kaffi

DESIGN · DEMOLITION · CAFFEINE · DISPATCH
EDITION 1031 · 31 October 2026
BROADCAST 04:42 CET
2,400 BROADSHEETS PRINTED
READ TIME · 47 MIN
2.90 m Floor to Floor: How Many Risers? A Swiss Stair Check in Python
ACADEMY
FRAME · 06:50
31-10-2026

2.90 m Floor to Floor: How Many Risers? A Swiss Stair Check in Python

Type a floor-to-floor height, try every riser count, test each against Blondel's rule and the Swiss BFU and SIA 500 bounds. 75 lines of plain Python.

A stair is one number. Before the handrail, the nosing detail or the oak-versus-terrazzo argument, somebody fixes the riser count n, and everything else follows from it. The riser height is the floor-to-floor height divided by n: s = H/n. The tread comes from a 17th-century stride rule. Two risers plus one tread make one human step, 2s + a = 63 cm, so a = 63 cm − 2s. According to Baunetzwissen, François Blondel set the rule down in his Cours d’architecture, which came out in parts, so sources give 1675 or 1683 for the rule. Either way, it has outlived every drawing tool ever used to apply it.

The Swiss rule sets do not replace Blondel. They put bounds around him, which turns the design problem into a loop over n. That is a program a beginner can write, read line by line, and own. Two risers plus one tread: the oldest API in architecture.

The Tool: Python 3 and its standard library. There is nothing to pip-install and only one import (math). Guido van Rossum started the language at CWI in Amsterdam in 1989, and the Python Software Foundation stewards it today. Python 3.15.0 shipped earlier this month, on 9 October, with Hugo van Kemenade as release manager. This script uses no 3.15 feature. The editor ran it on Python 3.10.11, and the file pastes unchanged into the Rhino 8 ScriptEditor, which runs CPython 3.

The rules come from people, not from the language. The main source is Beatrix Jeannottat, an architect who has advised on homes and products at the BFU since 2012. Her Fachdokumentation 2.007 Treppen (Bern, 2020) sets the two Swiss columns side by side in Table 1:

  • Private stairs (BFU recommendations): tread a of 26–29 cm and riser s of 17–19 cm, plus three formulas. The stride rule is 2s + a = 63 cm, the comfort rule is a − s ≈ 12 cm, and the safety rule is a + s = 46 cm.
  • Publicly accessible stairs (SIA 500 Hindernisfreie Bauten): tread ≥ 28 cm and riser ≤ 17.5 cm. For the three formulas, the column reads “Keine Angabe”.

The BAV checklist for cable cars (Version 2.0, July 2026) quotes the same SIA 500 figures under Ziff. 3.6.2. Under Ziff. 3.6.1 it adds that a flight of 16 steps or more needs an intermediate landing. BFU’s Table 4 puts that threshold at 16–18 risers inside buildings and gives the Podestformel for sizing the landing: ½a + n strides + ½a. One honest note: this piece reads SIA 500 through those two summaries, not from the norm itself. The script certifies nothing.

←TODAY: In 2026 the riser count is fixed on a first section, then defended for two years. →3012: In Zurich-3012 the stair is still drawn to the human stride, because the stride never got an update. Fulcrum: A 17th-century body rule and a 75-line loop meet at the one number you cannot change later.

Setup:

# Python 3.10 or newer; standard library only
python --version
mkdir stair-check
cd stair-check
# save the program below as stair_check.py, then run it:
python stair_check.py
# Rhino 8: type ScriptEditor, create a new Python 3 script,
# paste the same file and press Run

First steps:

  1. Save the program as stair_check.py and run it with the example values H = 2900 and L = 5000. The console prints a sanity line and two tables.
  2. Read the sanity line first. A 17/29 cm stair gives 630, 120 and 460, so it hits all three formulas exactly. Suva calls 17 : 29 “ideal”, and Sophie Jung recalls in Baunetzwoche #417 that Neufert’s Bauentwurfslehre names 17 × 29 cm as the most comfortable stair.
  3. Change H to your office’s floor height and L to the run your plan actually gives the stair, then run it again. The rows that survive are your options. Read the verdict column before the numbers.

The code, explained:

# stair_check.py - size a straight stair and test every candidate against the Swiss rules
# Standard library only. All lengths in mm. Pastes unchanged into Rhino 8 ScriptEditor (Python 3).
import math

H = 2900            # floor-to-floor height, finished floor to finished floor
L = 5000            # horizontal run available for the stair (first to last nosing), mm
STRIDE = 630        # 2 s + a target (BFU: 63 cm; range 59-65 cm)

RULES = {           # tread a min/max, riser s min/max (mm), landing needed from N risers
    "SIA 500 public access": dict(a=(280, 9999), s=(0, 175),   landing=16),
    "BFU private stair":     dict(a=(260, 290),  s=(170, 190), landing=16),
}

def three_rules(s, a):
    """Blondel stride rule, comfort rule, safety rule (BFU Fachdokumentation 2.007)."""
    stride = 590 <= 2 * s + a <= 650
    comfort = abs((a - s) - 120) <= 20          # a - s ~ 12 cm
    safety = 450 <= a + s <= 470                # a + s = 46 cm
    return stride, comfort, safety

def candidates(H, L, rule):
    """Try every riser count; tread from the stride rule, capped by the run available."""
    out = []
    for n in range(10, 25):                     # n risers -> n - 1 treads in a straight flight
        s = H / n
        a_rule = STRIDE - 2 * s                 # tread the stride rule asks for
        a = min(a_rule, L / (n - 1))            # but no more than the run allows
        run = a * (n - 1)
        ok_s = rule["s"][0] <= s <= rule["s"][1]
        ok_a = rule["a"][0] <= a <= rule["a"][1]
        out.append((n, s, a, run, ok_s, ok_a, n >= rule["landing"]))
    return out

def report(name, rule):
    print(f"\n=== {name}: tread {rule['a'][0]}-{rule['a'][1]} mm, riser {rule['s'][0]}-{rule['s'][1]} mm,"
          f" landing from {rule['landing']} risers ===")
    print(f"{'n':>3}{'s mm':>8}{'a mm':>8}{'2s+a':>7}{'a-s':>6}{'a+s':>6}{'deg':>6}"
          f"{'run m':>7}  verdict")
    for n, s, a, run, ok_s, ok_a, landing in candidates(H, L, rule):
        if not (ok_s and ok_a):
            continue
        stride, comfort, safety = three_rules(s, a)
        ang = math.degrees(math.atan2(s, a))
        notes = []
        if not stride:  notes.append("stride")
        if not comfort: notes.append("comfort")
        if not safety:  notes.append("safety")
        if landing:     notes.append("split flight + landing")
        verdict = "OK" if not notes else "check: " + ", ".join(notes)
        print(f"{n:>3}{s:8.1f}{a:8.1f}{2*s+a:7.0f}{a-s:6.0f}{a+s:6.0f}{ang:6.1f}"
              f"{run/1000:7.2f}  {verdict}")

def split_flight(n, s, a):
    """Two flights with a half landing: landing depth from the BFU Podestformel a + k*stride, k>=2."""
    n1 = math.ceil(n / 2); n2 = n - n1
    landing = a + 2 * STRIDE
    run = (n1 - 1) * a + landing + (n2 - 1) * a
    print(f"\nSplit {n} risers as {n1} + {n2}: riser {s:.1f} mm, tread {a:.0f} mm,"
          f" landing >= {landing:.0f} mm (a + 2 strides),")
    print(f"  total run {run/1000:.2f} m in a straight line, or {(max(n1, n2) - 1) * a / 1000:.2f} m"
          f" per flight if it turns 180 degrees.")

def site_check(s, finish_mm):
    """A finish laid on the upper floor only shifts the LAST riser. BFU trip threshold: 6 mm."""
    last = s + finish_mm
    print(f"\nSite check: {finish_mm} mm extra finish on the upper floor only -> last riser"
          f" {last:.1f} mm vs {s:.1f} mm, difference {finish_mm} mm"
          f" -> {'trip hazard (> 6 mm)' if finish_mm > 6 else 'within 6 mm'}")

print(f"Floor-to-floor {H} mm, run available {L} mm, stride target {STRIDE} mm")
print(f"Check: 17/29 cm -> 2s+a = {2*170+290} mm, a-s = {290-170} mm, a+s = {290+170} mm")
for name, rule in RULES.items():
    report(name, rule)
split_flight(17, H / 17, STRIDE - 2 * H / 17)
site_check(H / 17, 20)

Running python stair_check.py prints exactly this:

Floor-to-floor 2900 mm, run available 5000 mm, stride target 630 mm
Check: 17/29 cm -> 2s+a = 630 mm, a-s = 120 mm, a+s = 460 mm

=== SIA 500 public access: tread 280-9999 mm, riser 0-175 mm, landing from 16 risers ===
  n    s mm    a mm   2s+a   a-s   a+s   deg  run m  verdict
 17   170.6   288.8    630   118   459  30.6   4.62  check: split flight + landing
 18   161.1   294.1    616   133   455  28.7   5.00  check: split flight + landing

=== BFU private stair: tread 260-290 mm, riser 170-190 mm, landing from 16 risers ===
  n    s mm    a mm   2s+a   a-s   a+s   deg  run m  verdict
 16   181.2   267.5    630    86   449  34.1   4.01  check: comfort, safety, split flight + landing
 17   170.6   288.8    630   118   459  30.6   4.62  check: split flight + landing

Split 17 risers as 9 + 8: riser 170.6 mm, tread 289 mm, landing >= 1549 mm (a + 2 strides),
  total run 5.88 m in a straight line, or 2.31 m per flight if it turns 180 degrees.

Site check: 20 mm extra finish on the upper floor only -> last riser 190.6 mm vs 170.6 mm, difference 20 mm -> trip hazard (> 6 mm)

Lines 1–12 — the inputs, and the rules as data: Everything is in millimetres, so you never forget a ×1000. H and L are example inputs, nothing more. RULES is a dictionary, a lookup table that maps a name to a value. Each bound is a tuple, a fixed (min, max) pair, and 9999 stands in for “no maximum tread”. Storing rules as data instead of if statements means your office can add a third set, such as the SN 640 075 column for public outdoor stairs, without touching the logic.

Lines 14–19 — Blondel in three sentences: A function is a named block of code you can call again and again. This one returns three booleans, plain True/False answers. The 590–650 mm stride window is the adult range BFU prints. The ±20 mm comfort band and the 450–470 mm safety window are the editor’s tolerances, read against BFU’s “≈ 12 cm” and 45–47 cm. They are not norm text, so set your own.

Lines 21–32 — the loop that is the whole algorithm: A loop means “do this once for each value”. for n in range(10, 25) tries every riser count from 10 to 24. s = H / n is the riser, STRIDE - 2 * s is Blondel rearranged, and min(a_rule, L / (n - 1)) keeps that tread unless the plan has no room. That one min explains row 18: the rule asks for 307.8 mm, but 5,000 / 17 = 294.1 mm is all the run allows, so 2s + a drops to 616, still inside 59–65 cm. The beginner’s mistake sits in the comment on line 24. In a straight flight, n risers give n − 1 treads, because the upper floor is the last “tread”. Divide by n and the run comes out one tread short, and nobody notices until the Bauleitung measures.

Lines 34–51 — the report: The f before a string makes it an f-string: {s:8.1f} drops in a number eight characters wide with one decimal, which lines up the columns. continue skips candidates outside the bounds. math.atan2(s, a) gives the pitch in radians, and math.degrees converts it. Now the twist. Under SIA 500, n = 16 gives 181.2 mm, above 175, so only 17 and 18 survive, and both cross the 16-riser landing threshold. Under BFU, 16 passes the bounds but fails comfort (86) and safety (449). At 2.90 m there is no clean straight flight in either set. The stair has opinions, and they are 17th-century ones.

Lines 53–75 — landing, site, run order: math.ceil rounds up, so 17 risers split as 9 + 8. The Podestformel with two strides gives 288.8 + 1,260 = 1,549 mm of landing. Straight, that stair needs 5.88 m, more than the 5.00 m available, which pushes you to a dog-leg at 2.31 m per flight. Then site_check: a 20 mm finish laid on the upper floor only makes the last riser 190.6 mm against 170.6 mm everywhere else.

Why the last line matters

The BFU measured 529 public stairs in 2023, 242 indoor and 287 outdoor, across Basel, Bern, Lausanne and Zürich. According to its press release of 12 January 2024, the steps on more than two-thirds of them differed in height by at least 6 mm, and the BFU states that 6 mm is already enough to cause trips. The same release counts more than 90 deaths and more than 50,000 injuries from stair falls in Switzerland every year. That data is now nearly three years old, and it exists because people walked those stairs with tape measures. They are the reason line 75 exists.

The trade-off is plain: the script checks the stair you drew, not the screed someone lays after every drawing has passed.

Atelier: In a twelve-person Zürich studio, the riser count is set on the first Vorprojekt section, while the floor build-ups keep shifting until the Ausführungsplanung and the stair is expected to absorb the difference. Monday move: put stair_check.py in the office template folder with your standard floor-to-floor heights filled in, and draw no stair core until both floors’ finished build-ups have gone through site_check.

Hack: Find out how thin a one-sided finish must be before the script stops warning you. Change 20 mm to 5 mm and predict the last line before you press Run.

# replace the last line of stair_check.py with:
site_check(H / 17, 5)        # 5 mm finish instead of 20 mm
# expected last line ends with:
# last riser 175.6 mm vs 170.6 mm, difference 5 mm -> within 6 mm

Then look at finish_mm > 6. BFU says 6 mm already trips people, yet a 6 mm finish still prints “within 6 mm”. Whether to tighten it to >= 6 is your office’s call. Make it in the code, where the next person can see it.

PAZ Academy teaches this script-first habit in its Rhino 8 and Grasshopper 2 courses. The same file later becomes a Script component that reads H straight off your section, but it starts here, as plain Python you can read end to end.

Learn-it:

Open the section on your desk, read off H, and run the check before anyone draws the first tread.

FILED FROM
CO-SIGNERS
PAZ Academy
CONFIDENCE
HIGH
REPRINTS
© PAZ - PARAMETRIC ACADEMY ZURICH · ALL RIGHTS RESERVED

PAZ Kaffi · multidisciplinary editorial, led by PAZ Academy

⚑ REPORT AN ERROR · SUBMIT A CORRECTION
◂ BACK TO FRONT PAGE · PAZ KAFFI

© 2026 PAZ Academy.