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

PAZ Kaffi

DESIGN · DEMOLITION · CAFFEINE · DISPATCH
EDITION 1107 · 7 November 2026
BROADCAST 04:42 CET
2,400 BROADSHEETS PRINTED
READ TIME · 47 MIN
Type an Address, Get Your Roof's Snow Load: SIA 261 in Python
ACADEMY
FRAME · 06:50
07-11-2026

Type an Address, Get Your Roof's Snow Load: SIA 261 in Python

A 65-line Python script turns a Swiss address into terrain height via swisstopo's free API and applies the SIA 261 snow formula, self-checked against a court.

Snow load on a Swiss roof is one formula and one altitude. The formula is sk = 0.4 · [1 + (h0/350)²] kN/m², never less than 0.9, and the part worth watching is the square. Altitude does not add snow to a roof. It multiplies it. Bern sits at 543 m and Davos at 1,557 m: 1,014 m more, and six times the load.

First snow is never far off in November, and in many offices the site altitude still gets typed in by hand. This tutorial replaces that keystroke with a 65-line, standard-library Python script. It asks swisstopo for coordinates and terrain height, applies the formula and the roof factors, and checks its own arithmetic against a court decision before it prints anything you might believe.

The formula is quoted here the way a public body applied it. The Baurekursgericht des Kantons Zürich worked through it in decision BRGE IV Nr. 0169/2013 of 21 November 2013, for a building at 536 m. That is an older case, and it applied SIA 261:2003. According to the Baurekursgericht, sk is at least 0.9 kN/m², “roughly 90 kg/m²”, and the load on plan, qk, is sk multiplied by the roof-shape factor μ, the wind-exposure factor Ce and the thermal factor Ct. The altitude offset Z and the 2,000 m validity limit come from the SIA 261:2014 summaries on dewiki and in Dlubal’s load-zone tool.

The Tool: swisstopo’s GeoAdmin API, the REST interface behind the federal geoportal. It is free and needs no key. The script uses two endpoints: SearchServer, which turns text into LV95 coordinates, and height, which returns the terrain elevation at a point. The people at the Federal Office of Topography in Wabern keep a whole country’s terrain answering HTTP calls in milliseconds. Nobody on site thanks them, because it simply works.

Setup: Python 3.10 or newer, nothing to install. On Windows PowerShell, type curl.exe instead of curl.

python --version            # tested with 3.10.11
mkdir snow
cd snow
# prove the height endpoint answers (LV95 point near Bern)
curl "https://api3.geo.admin.ch/rest/services/height?easting=2600000&northing=1200000&sr=2056"
# expected roughly: {"height":"555.5"}
# save the program below as snow_load.py, then:
python snow_load.py

First steps:

  1. Run the curl line. swisstopo’s “Get Point Height” documentation shows 553.6 for that point; our live call on 3 November 2026 returned 555.5. The elevation model gets updated, so a metre or two of drift is normal.
  2. Save the program as snow_load.py and run it. If the first line does not say 1.338 and 1.07, stop and find out why.
  3. Replace the example addresses in SITES with your parcel’s street address.
  4. Set Z to the value you read off SIA 261 Annex D. Z = 0 is a placeholder, not an answer.

The code, explained: every line is a network call, a formula or a print statement.

# snow_load.py - characteristic snow load for a Swiss address, from text to kN/m2
# Standard library only. Terrain height: swisstopo GeoAdmin API (free, no key). Formula: SIA 261.
import json, re, urllib.parse, urllib.request

API = "https://api3.geo.admin.ch/rest/services"

def get(path, **params):
    url = f"{API}/{path}?{urllib.parse.urlencode(params)}"
    with urllib.request.urlopen(url, timeout=20) as r:
        return json.load(r)

def locate(text, origins="address"):
    """Search an address (or a municipality with origins='gg25').
    NOTE: in the answer, y is the EASTING and x the NORTHING (LV95)."""
    hit = get("ech/SearchServer", searchText=text, type="locations", origins=origins,
              sr=2056, limit=1)["results"][0]["attrs"]
    return hit["y"], hit["x"], re.sub("<[^>]+>", "", hit["label"])

def height(e, n):
    return float(get("height", easting=round(e, 1), northing=round(n, 1), sr=2056)["height"])

def sk(h, z=0):
    """SIA 261: sk = 0.4 [1 + (h0/350)^2] kN/m2, at least 0.9; h0 = h + Z (Z from the Annex D map)."""
    h0 = h + z
    return max(0.9, 0.4 * (1 + (h0 / 350) ** 2)), h0

def roof(skv, mu=0.8, ce=1.0, ct=1.0):
    """qk on plan = mu (roof shape) * Ce (wind exposure) * Ct (thermal) * sk."""
    return mu * ce * ct * skv

def tonnes(q, area=120):                # a 10 x 12 m roof, kN -> t
    return q * area / 9.81

# Self-check: Zurich Baurekursgericht, 21 Nov 2013, building at 536 m, ~14 deg pitched roof
s, _ = sk(536)
print(f"Self-check 536 m: sk {s:.3f} kN/m2 (decision: 1,338), qk {roof(s):.2f} (decision: 1,07)")

SITES = ["Marktplatz 9 4051 Basel", "Bundesplatz 3 3011 Bern", "Bahnhofplatz 1 7000 Chur",
         "Gotthardstrasse 1 6490 Andermatt", "Promenade 67 7270 Davos", "Via Maistra 12 7500 St. Moritz"]
Z = 0          # EXAMPLE: read your site's Z (-200/0/+200/+400/+500 m) off SIA 261 Annex D
print(f"\n{'address':<32}{'E':>9}{'N':>9}{'h m':>7}{'sk':>7}{'qk':>7}  t on 10x12 m")
for text in SITES:
    e, n, label = locate(text)
    s, h0 = sk(height(e, n), Z)
    note = "  h0 > 2000 m: formula not valid" if h0 > 2000 else ""
    print(f"{label[:31]:<32}{e:9.0f}{n:9.0f}{h0:7.0f}{s:7.2f}{roof(s):7.2f}{tonnes(roof(s)):9.1f}{note}")

print("\nWhat 1 kN/m2 of sk looks like (densities as in the 2013 decision / SLF):")
for kind, rho in (("fresh snow", 100), ("settled snow", 200), ("wet snow", 400)):
    print(f"  {kind:<13}{rho:>4} kg/m3 -> {1000 / 9.81 / rho * 100:5.0f} cm")

print("\nWhat Z does in Davos (same address, Annex D value is YOURS to read):")
e, n, _ = locate("Promenade 67 7270 Davos")
h = height(e, n)
for z in (-200, 0, 200):
    s, h0 = sk(h, z)
    print(f"  Z {z:+4d} m -> h0 {h0:5.0f} m, sk {s:5.2f}, qk {roof(s):5.2f} kN/m2")

# Site check: type only the town name and you get the municipality's point, not your parcel.
for town, addr in (("Chur", "Bahnhofplatz 1 7000 Chur"), ("Andermatt", "Gotthardstrasse 1 6490 Andermatt")):
    (e1, n1, l1), (e2, n2, _) = locate(town, origins="gg25"), locate(addr)
    h1, h2 = height(e1, n1), height(e2, n2)
    q1, q2 = roof(sk(h1)[0]), roof(sk(h2)[0])
    print(f"Site check '{town}': municipality point {h1:.0f} m -> qk {q1:.2f}; address {h2:.0f} m"
          f" -> qk {q2:.2f} kN/m2 ({tonnes(q1) - tonnes(q2):+.0f} t on 10x12 m)")

Lines 1–10 — one door to the API: get() is the only function that touches the network. urlencode makes parameters URL-safe, umlauts included; the 20-second timeout makes a dead connection fail instead of hang; json.load turns the plain-text reply into Python dictionaries. If the service address ever changes, you change one line.

Lines 12–20 — the swap: according to swisstopo’s search documentation, in SearchServer answers x is the northing and y the easting; their “wabern” example returns x 197515.6 and y 601112.8. That is why locate() returns hit["y"] first. This is the beginner mistake of the piece: swap them and you ask for the height of somewhere else. The tell: LV95 eastings start with a 2, northings with a 1. The regex (a text pattern) strips HTML tags from the label, which the script prints instead of your query, because “4051 Basel” comes back as the postal alias “4001 Basel”.

Lines 22–36 — the formula and its check: sk() adds Z to the terrain height to get h0, and max(0.9, …) picks the larger number, enforcing the minimum. roof() uses μ = 0.8, Ce = 1 and Ct = 1, the values the 2013 decision used for its pitched roof of about 14°, not values for your roof. tonnes() converts to tonnes on a 10 × 12 m roof. Then the script recomputes the court’s case: Self-check 536 m: sk 1.338 kN/m2 (decision: 1,338), qk 1.07 (decision: 1,07). A test is just code that knows the answer in advance.

Lines 38–47 — six addresses, in tonnes: the live run on 3 November 2026 gave qk on the roof of 0.72 kN/m² in Basel (253 m), 1.09 in Bern (543 m), 1.21 in Chur (585 m), 5.66 in Andermatt (1,430 m), 6.65 in Davos (1,557 m) and 9.08 in St. Moritz (1,832 m). Check Bern by hand: (543/350)² = 2.407, so 0.4 × 3.407 = 1.36, and × 0.8 = 1.09. Basel’s raw formula gives 0.61, so the minimum governs. The same footprint carries 8.8 t in Basel and 111.1 t in St. Moritz. Lines 49–51 make a kN/m² physical. Jan Eitel reports for SRF Meteo, citing the SLF, that fresh snow weighs up to about 100 kg/m³ and wet snow over 400. One kN/m² is therefore about 102 cm of fresh snow, 51 cm settled or 25 cm wet.

Lines 53–58 — Z is yours: at the same Davos address, Z = −200 m gives qk 5.13 kN/m², Z = 0 gives 6.65 and Z = +200 m gives 8.39. That is 3.26 kN/m² from one map reading, which is why the script refuses to guess Z.

Lines 60–66 — search the address, never the town: origins="gg25" asks for the municipality’s reference point. For “Chur” that sits at 1,459 m against 585 m at Bahnhofplatz: qk 5.88 instead of 1.21, or +57 t. For “Andermatt” it is 1,983 m against 1,430 m: 10.59 against 5.66, or +60 t. Precise to two decimals, wrong by a mountain. Line 46 flags h0 above 2,000 m because, according to dewiki, the formula is valid only up to that height; higher sites need a snow report.

The script is fast because it is narrow: terrain height at one point, one formula, no drifts, valleys or parapets. That narrowness is the risk: a quick wrong altitude is more dangerous than a slow right one. Treat it as a pre-design check; the engineer of record designs the roof. It runs unchanged in the Rhino 8 ScriptEditor, and the natural next step is feeding it the parcel centroid from your Grasshopper site model.

←TODAY: swisstopo answers a terrain-height query for any Swiss point in one free HTTP call.
→3012: the parcel’s altitude and its load logic travel with the project as readable text, long after the software that drew it.
Fulcrum: open public data plus a script you can read line by line beats a number you have to trust.

From the late 2070s the pattern is plain: the calculations that survived were plain text against open public data, still readable by a 25-year-old decades later. This is that kind of file.

Atelier: a small studio in Graubünden or the Bernese Oberland meets snow load in the first site meeting, usually as a number someone remembers. Now the altitude comes from the federal model and the arithmetic is visible to the whole team, including the engineer who signs. Monday move: put snow_load.py into the project template folder and add one checklist line: “parcel address run, Z read off Annex D, printout handed to the engineer”.

Hack: prove the square to yourself before you trust the table. Paste these three lines at the bottom and predict first: doubling 500 m to 1,000 m should roughly triple sk, and doubling again to 2,000 m nearly quadruple it. Expect 1.22, 3.67 and 13.46 kN/m².

for h in (500, 1000, 2000):
    s, _ = sk(h)
    print(h, "m ->", round(s, 2), "kN/m2")

If your numbers match, you have read the formula, not just copied it.

Want the result written straight into Archicad properties? That is the ground PAZ Academy’s self-study course Von Dokumentationslücken zu smarter Automatisierung mit Python & Archicad covers, starting from zero Python.

Learn-it:

Save the file, swap in the address of the parcel on your desk, and run it before the first snow does the measuring for you.

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.