Class 3 of 6 Unit 2 · Working like an engineer 4 hours · 2 at the keyboard
Data, Structure and Keeping the Work
Choosing the shape, the growth curve that ambushes you, messy real exports, and version control as an undo you can trust.
A 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:
| 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.
# 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] = f
Same 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#
You do not need complexity theory. You need four curves and the instinct to ask which one you are on.
| 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#
Here is a genuine line from an exported fixture list:
"Mac Aura XB",1,"137",FOH Bar 2, 4.5,"Colour, Zoom",,TRUE
Count 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#
| 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 |
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.
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 back
Five 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#
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 |
Self test with model answers
Answer it yourself first, out loud or on paper, then open the answer. Reading the answer without attempting the question teaches you almost nothing.
-
It is n², so 16 million comparisons where 4000 would do. It is invisible in testing because you tested on a small file, and it appears on the real rig on the get-in. Replace it with a dictionary keyed on address: one pass, checking whether each address has been seen, which is a lookup that does not care how large the dictionary is. The rewritten version is also shorter, which is usual: choosing the right shape and simplifying are frequently the same edit.
-
A comma inside a quoted field, which splitting on commas destroys. A number stored as text. An empty field that could mean zero, missing or unfilled. A header row somebody edited. The rule: use the language’s CSV library rather than splitting yourself, convert types at the boundary and complain about failures, decide in writing what empty means, and never modify the original file.
-
Git keeps a full history wherever the repository is. One copy on one machine is still one copy: the flight case, the theft or the failed drive takes the history with it. The smallest fix is a remote, pushed to routinely. Note also what git is: it stores snapshots, not differences, so any commit can be recovered exactly, which is why committing often costs eight seconds and is the real undo.
-
Reintroducing the fault. If putting the change back makes the symptom return, you have proved cause. If it does not, something else changed and the original fault is still there, waiting for the day. It is the step everybody skips because the symptom has already gone, and it is the same discipline as proving a fix on a bench with a meter.
-
Versions in the filename, which means there is no single source of truth and somebody will open the wrong one. No date in a sortable format, so alphabetical order tells you nothing about which is current. No README, so nobody knows what it is, how to run it or who to ask. Each costs the same thing in the end: an hour of somebody’s time during a get-in, spent establishing something that could have been written down once in ten lines.
Note. These are model answers, not the only correct ones. If yours differs and you can defend it with the arithmetic and a measurement, that is worth more than matching the wording.
The four hours, accounted for. This is the lecturer's view of the session, and it is here rather than hidden in a pack because a student who can see where the time goes can see what the class is actually for.
Run of the session #
240 minutes · 130 at the keyboard · 75 on the idea
- 10min Open The file disaster stories, and what each one would have cost
- 25min The idea Choosing a shape: list, dictionary, set, and what each is fast at
- 15min Lab · gym Logic gym 3, on paper. Shape, growth, and the invariant
- 20min The idea Growth: why a method that works on 50 fixtures dies on 5000
- 15min Break —
- 40min Lab A Parse the messy CSV you brought, and count the ways it is messy
- 15min The idea Formats, and the rule that picks one
- 35min Lab B Version control on a real folder: commit, branch, break it, get it back
- 40min Lab C Bisect a fault in code you did not write, on a budget of six questions
- 15min The idea Naming and the show-file discipline that survives a crew change
- 10min Close The one command you will use tomorrow
Blocks are an order of work rather than a timetable: a class that runs long at the keyboard is a class that is going well. The minutes are here so you know what you are trading against when it does.
Four hours goes fast, and most of it is at the keyboard. What you do before the class, and in particular whether the software is installed and working, decides whether you spend those hours learning or catching up.
What you must already be able to do#
Write a function with parameters and a return value, and read somebody else's signature. Convert text to a number and know why you had to. Explain the difference between a value and a reference, and why it caused the shared-cue bug in Class 2.
Three things to do#
- Export something real. A fixture list, a cue list, a schedule, a stock list. Get it out of whatever software owns it, as CSV. Open it in a text editor, not a spreadsheet, and look at what is actually in the file. Bring it. We will parse the ones the class brings, and they will be messier than any example I could invent, which is the point.
- Install git and make one commit.
git initin a folder, add a file,git commit. If it asks for a name and an email, give it one. That is the whole task. Bring the output ofgit log. - Write down the worst thing that has happened to a file you needed. Overwritten, lost, three versions and nobody knew which was current, saved on a machine that went back to the rental company. One paragraph. Everybody has one and it is the argument for the second half of the class.
What to bring#
Laptop with Python and git. Your exported CSV. The file disaster story.
The calculations this class asks for, with the working shown. Use them to check your arithmetic, never to replace it: the assessment asks for the method.
Growth: what happens at scale
Storage and transfer time
Number bases, bits and ranges
Retrieval, not rereading. Everything here is self-graded, stored in this browser only, and reported to nobody.