Class 3 · Data, Structure and Keeping the Work
4 h · 2 at the keyboard 00:00A production is a data problem wearing a costume. The fixtures, the cues, the frames, the crew hours and the truck are all lists of things with relationships between them, and every hour lost to a spreadsheet is an hour lost to a badly chosen shape.
Logic gym#
Fifteen minutes, on paper, in pairs. Same rule. Out loud, in a sentence, before you check.
1. Pick the shape, and say the cost#
For each of these, name the shape and say what the expensive operation would then be:
- Every fixture in the rig, in patch order, printed on a plot.
- "Is address 214 already used?", asked five thousand times.
- Every cue in the show, looked up by cue number.
- The list of departments that have signed off, where asking twice is meaningless.
The second answer is the interesting one. Say how long the list version takes on 5 000 fixtures against the set version, in orders of magnitude rather than in seconds.
2. Fifty becomes five thousand#
A check compares every fixture with every other one. On 50 fixtures it is instant.
- How many comparisons on 50?
- On 500?
- On 5 000?
Say the three numbers, then say the shape of the curve in one word. Then say the thing that actually happens in a building when a check gets slow, which is not "people wait".
3. Name the invariant#
"Every fixture in this rig has a unique address range, and every range fits inside one universe."
That is an invariant: a thing that must be true before and after any change. Write two more for a show file you have worked on. Then, for each, say what breaks first when it stops being true, and whether anybody would notice.
4. Where is the fault#
Forty commits between "it worked" and "it does not". You may ask "was it good at commit N?" and you get a yes or no.
Name the commit you would ask about first, and say how many questions you need in the worst case. Then do the same for a thousand commits. If your second answer is not about ten, you are walking the list.
5. The row you would have dropped#
A cast list export has: a blank name, a duplicate name, a name with a trailing space, and a role field containing a comma.
For each, say keep, reject, or keep and report, and say who is harmed by the wrong choice. One of the four has a different right answer from the others and it is the one that costs somebody their entrance.
What the gym is for#
Class 1 held behaviour, Class 2 held values. This one holds shape and scale, which is the pair a technical director is asked about in a meeting: will it still work when there are ten times as many, and what happens to the row we cannot read.
Choosing the shape of your data#
Most bad show tooling is not badly written. It is written on the wrong shape, and every operation afterwards has to fight it.
Three shapes cover nearly everything you will do.
A list is ordered and you get at things by position. A cue stack is a list: cue 3 comes after cue 2, and that order is the meaning. Finding a specific item means looking through it from the start.
A dictionary (a map, an object, a hash) is unordered and you get at things by key. The patch is a dictionary: address 137 gives you the fixture at 137, immediately, without looking at the other 511.
A set holds unique things and answers one question fast: is this in it? Use it for "which addresses are already taken", where the only question is membership.
The cost table is the whole reason to care:
Choosing the shape of your data#
| Operation | List | Dictionary | Set |
|---|---|---|---|
| Get item by position | Instant | n/a | n/a |
| Find by value or key | Look through all of it | Instant | Instant |
| Add to the end | Instant | Instant | Instant |
| Remove from the middle | Shift everything after it | Instant | Instant |
| Keep in order | Yes, that is the point | Insertion order only | No |
The mistake that appears in almost every first tool: keeping the patch as a list of fixtures and searching it every time you need one. With 40 fixtures it is instant. With 4000 fixtures, and a check that runs once per fixture, you are doing 16 million comparisons and wondering why the script takes a minute.
Choosing the shape of your data#
# The shape that fights you
for f in fixtures: # 4000
for g in fixtures: # times 4000
if f.address == g.address and f is not g:
print("clash", f.address)
# The shape that does not
seen = {}
for f in fixtures: # 4000, once
if f.address in seen:
print("clash", f.address, "between", seen[f.address].name, "and", f.name)
seen[f.address] = fSame answer. The second is roughly four thousand times less work, and it is shorter. Choosing the right shape is usually the same edit as making it simpler.
Growth, and why 50 becomes 5000#
Growth, and why 50 becomes 5000#
You do not need complexity theory. You need four curves and the instinct to ask which one you are on.
Growth, and why 50 becomes 5000#
| Shape | Name | 100 items | 10 000 items |
|---|---|---|---|
| n | Linear: touch each item once | 100 | 10 000 |
| n log n | Sorting | ~700 | ~130 000 |
| n² | Every item against every other | 10 000 | 100 000 000 |
| 2ⁿ | Try every combination | Beyond astronomical | Never |
The practical rule: anything with a loop inside a loop over the same data is n², and it is the one that ambushes you. It is invisible in testing, because you test on the small file. It appears on the real rig, on the get-in, on the day.
The other practical rule: computers are fast enough that n² is often fine. Four thousand fixtures squared is sixteen million simple operations, which is about a fifth of a second. Twenty thousand LED pixels squared is four hundred million, which is thirty seconds and a phone call. Know which side of that line you are on before you find out on the day.
Do it now: the clash check that still works at five thousand#
Write it any way you like. The tests only check the answer, which is deliberate: get it right first, then make it cheap. The obvious version compares every fixture with every other one, which on fifty fixtures is 1 225 comparisons and instant, and on five thousand is twelve million and slow enough that people stop running the check.
Then open the growth calculator and put your version's cost against a real festival patch. The third hint gives you the shape that grows with the number of fixtures rather than with its square, and the difference between those two sentences is most of what this section is for.
Real data is messy, and that is the job#
Real data is messy, and that is the job#
Here is a genuine line from an exported fixture list:
"Mac Aura XB",1,"137",FOH Bar 2, 4.5,"Colour, Zoom",,TRUECount what a program has to survive:
- Quotes around some fields and not others, because the exporter quotes anything containing a comma
- A comma inside a quoted field, which naive splitting on commas destroys
- A number stored as text (
"137") - Leading whitespace before
4.5 - An empty field, meaning either "no value" or "nobody filled it in", which are different things
TRUEas text, not a boolean- No indication of what any column is unless the header row is right, which it often is not
Rules that hold up:
- Never split a CSV on commas yourself. Use the library. Python has
csvbuilt in. The library knows about quoting, escaping and line breaks inside fields, and you do not. - Convert types at the boundary. The moment data enters your program, turn text into numbers and booleans, and complain loudly about anything that will not convert. A wrong value discovered at row 3 on the way in costs a minute. Discovered at row 2000 on the way out, it costs the day.
- Decide what an empty field means, in writing. Zero, missing, or an error. All three are reasonable and picking silently is what makes two departments' numbers disagree.
- Never modify the original. Read it, write a new one. Your script will have a bug on the first run. Every script does.
Do it now: the export nobody cleaned#
Five cases. The fourth is the whole exercise: a duplicate name is usable and must be reported. A tool that silently drops it produces a document that looks correct and is not, and the person who finds out is the understudy standing outside a locked door.
Rejecting and reporting are different actions. Most hurried code does one and calls it both.
Formats, and picking one on purpose#
Formats, and picking one on purpose#
| Format | Good at | Bad at | Use it for |
|---|---|---|---|
| CSV | Tables, universal, opens in Excel | Anything nested, no types, no standard | Fixture lists, schedules, anything the office also needs |
| JSON | Nesting, types, every language reads it | Human editing, no comments | Configuration, network messages, saved state |
| YAML | Humans writing it, comments allowed | Indentation errors, surprising type rules | Configuration a human maintains by hand |
| XML | Old, strict, schema-checkable | Verbose, painful by hand | Whatever the industry standard already uses |
| SQLite | Querying, many records, safe concurrent reads | Not readable in a text editor | Anything past a few thousand rows you query repeatedly |
Formats, and picking one on purpose#
The one rule worth memorising: if a human will maintain it by hand, choose YAML or CSV. If a program will write it and another program will read it, choose JSON. If you are querying it, put it in SQLite and stop pretending a spreadsheet is a database.
That last one deserves a sentence. A spreadsheet is a wonderful place to look at data and a terrible place to keep it: there is no rule stopping somebody typing text in a number column, no record of who changed what, and the file locks when one person opens it. Every production office in the world is running on one anyway. Knowing exactly why that hurts is what lets you propose something better without sounding like you are proposing extra work.
Version control: the thing that would have saved your file#
Your file disaster story from the preparation has the same shape as everybody else's. Version control is the industrial answer, and it is worth about four commands.
A repository is a folder whose entire history is kept. A commit is a snapshot of every file at one moment, with a message saying why. A branch is a line of commits you can start and abandon without touching the main one. Merging brings a branch back.
The mental model that makes it click: git does not store differences, it stores snapshots. Every commit is the whole state of the folder. You can go back to any of them, exactly, forever.
Version control: the thing that would have saved your file#
git init # this folder now has a history
git add . # stage everything as it currently is
git commit -m "Patch as built, pre-tech" # snapshot it, with a reason
git log --oneline # what happened, most recent first
git checkout a3f9c21 # go and look at that snapshot
git checkout main # come backFive commands cover 90 per cent of solo use. The whole field card has the rest.
What actually goes wrong, and why it is survivable#
"I broke everything." git checkout . throws away uncommitted changes and returns to the last commit. The work since your last commit is gone; everything before it is safe. This is the argument for committing often.
"I need the version from Tuesday." git log to find it, git checkout to look, copy out what you need. Nothing is ever overwritten.
"Two of us changed the same file." That is a merge conflict, and git will mark both versions in the file and refuse to guess. It is not a failure, it is git declining to make an editorial decision that is yours.
The commit message is the part that matters#
git commit -m "changes" is worthless. In six weeks it tells you nothing, and in six weeks is exactly when you need it.
Write the message for the person who will read it during a technical rehearsal, at speed, looking for the change that broke it. That person is you.
"Fix DMX offset: fixtures were one channel low because the patch is 1-based and the array is 0-based" is a message that saves an hour later. It costs eight seconds now.
What version control does not do#
It is not a backup. A repository on one laptop that falls in the dock is one laptop's worth of loss. Push it somewhere else, or it is history you kept in exactly one place.
Debugging as a method, not a mood#
Debugging as a method, not a mood#
Electronics for Theatre, which I also teach, does fault-finding on a bench: establish the boundary, change one thing, prove the fix by putting the fault back. Code is the same discipline with faster instruments and no smell of hot plastic to warn you.
1. Reproduce it, exactly. A fault you cannot reproduce is a fault you cannot prove you fixed. Write down the exact input and the exact steps. Half of all "intermittent" bugs turn out to be perfectly reliable once somebody writes down what they were actually doing.
2. Establish the boundary. What works, what does not, and where the line is. Print the value before it goes wrong and after. Bisect: put a print statement halfway through, learn which half contains the fault, repeat. Ten halvings gets you through a thousand lines.
3. Read the error message. All of it, from the bottom. A Python traceback names the file, the line and the type of error. Most people scroll past it because it looks like noise. It is the answer, written down, for free.
4. Change one thing. Two changes at once and you have learned nothing about either.
5. Prove it by reintroducing the fault. Put the bug back. If the symptom returns, you found the cause. If it does not, you changed something else and got lucky, and it will come back on the day.
That last step is the one everybody skips and the one that separates a fix from a coincidence.
The three questions that solve most of them#
- Is it running the code I think it is? The wrong file, the wrong machine, an unsaved buffer, a cached build. Embarrassingly often. Put a deliberate print at the top and see it appear.
- Is the data what I think it is? Print it. Not its length, not a summary. The actual value and its type. Most bugs are text where you expected a number.
- Did it ever work? If yes, what changed? Git answers that one exactly, which is one more reason for the previous section.
Do it now: halve it, do not hunt it#
Forty commits, six questions. The starter walks the list, which is correct and far too slow, and the tests will fail it on the budget rows for exactly that reason.
This is git bisect, written out. Once you have built it once, the command stops being magic and becomes something you reach for at 23:40 when a show file worked on Tuesday and does not now. A thousand commits in ten questions is why it is a command rather than advice.
Keeping the work: files, names and the crew change#
Everything above is undone by a folder nobody else can navigate. The discipline is dull and it is what makes the difference between a system that survives you and one that does not.
Name files so they sort correctly. 2026-03-14 lx plot v3.pdf sorts by date automatically. FINAL_lx_plot_v3_REALLY_FINAL.pdf sorts by despair. Dates as YYYY-MM-DD, always. This is ISO 8601 and it exists precisely so that alphabetical order and chronological order are the same order.
Never put the version in the filename if you have version control. Pick one system. Two systems is no system.
Write down where the source of truth is. For every artefact: which file, on which machine, is the one that is right? An hour spent writing that down is the cheapest insurance in production.
One README in every project folder. What this is, how to run it, what it assumes, who to ask. Ten lines. It is the difference between a tool and a liability.
A tool nobody else can run is a tool that exists only until you are unavailable, and you will be unavailable exactly when it matters.
Numbers from this class#
| Thing | Value |
|---|---|
| Finding an item in a list of n | Up to n comparisons |
| Finding an item in a dictionary | One lookup, regardless of size |
| A loop inside a loop over the same data | n², the shape that ambushes you |
| 4000 items, compared every pair | 16 million operations, about 0.2 s |
| 20 000 items, compared every pair | 400 million operations, about 30 s |
| Sorting n items | n log n |
| What a git commit stores | A snapshot of every file, not a difference |
| Commands covering 90 per cent of solo git | init, add, commit, log, checkout |
| Date format that sorts correctly | ISO 8601: YYYY-MM-DD |
| Bisections to search 1000 lines | About 10 |
| Where to convert text to numbers | At the boundary, on the way in |
| What an empty CSV field means | Whatever you decided in writing. Otherwise, a future argument |
| Copies of a repository needed for it to be a backup | At least two, on different machines |
| Lines in a project README | About ten, and never zero |
↑↓ move · ↵ open · esc close