Finally! The Terminal-Based Flash Card System the World So Badly Needs
Tags: beginners, python, codenewbie, terminal
Published: 2023-05-23 21:56:05

## The Backstory

_What follows goes into great detail about the intricacies of analogue photography with a manual flash. If that's of no interest to you, just read the TL;DR and skip to the next heading, 'PyFlashCards: The Lightweight, Sort-of-Convenient Tool for Creating and Testing Yourself on Flash Card Decks.'_

```
TL;DR: I recently took up analogue photography again
after a decade away. If I want to take photographs
in low light conditions, this means I have to get to
grips for the first time with manual flash. To do
that, I need to internalize the exponential relation-
ships between multiple physical variables. To do that,
I need – flash cards. Thus was born my first
Codecademy CS101 portfolio project.
```

This January, after some months of talking myself into it, I took my first steps toward getting back into analogue photography – really, into any kind of photography not using a smartphone camera. My ejection from that world came summarily just under 11 years ago when, in the space of a few weeks, both my trusty Nikon D40 DSLR and my beloved Olympus OM-4 film SLR, a hand-me-down from my father, fell from different bags on different days and, in one essential way or another, ceased to function. I was coming to the end of my time at university; I had too many hobbies and interests in any case, and was about to have even less time for them as I attempted to enter the workforce. No doubt I could have afforded to replace my cameras in the years that followed had I stopped buying tobacco, or bought fewer drinks on nights out, but it seemed, somehow, convenient – even thoughtlessly so – just to let one interest go.

What changed? In the decade that followed, almost everything but my job, the sort of dream job that overlaps so much with one of your key artistic pursuits that, as an unforeseen but in hindsight predictable consequence, it becomes less and less a source of relaxation with every passing year. I realized, in short, that I had a need again for a hobby that actually _was_ a hobby. Back at my parents' house for Christmas, I dug out my old broken DSLR, and found that the prime lens I'd saved up for as a teenager – the Nikkor 50mm f/1.8 AF-D, a beautiful budget option that rivals lenses ten times the price for sharpness – was still in perfect working order. I returned to the capital; I travelled to Athens for a trip with friends which I largely spent looking envyingly on at their various DSLRs, Leicas and vintage rangefinders; and then, back at home, I threw myself into eBay. A few weeks later, I had a very handsome 1984 Nikon F3 to call my own. Another three months on, I had also invested in a 1956 Leica IIIF. After a decade in the kingdom of the imagination, I was decidedly returned to the realm of gadgets, gizmos and things.

![Digital photograph of a Nikon F3HP with Nikkon 50mm 1.8D lens and a brown leather strap](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n87zenvrhviuxl2w0400.jpg)![Digital photograph of a Leica IIIF RDST with Canon 50mm 1.8 LTM lens and a lens hood](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p86k9085tshocyg04jz5.jpg)

Here we begin to approach the foothills of my choice of a portfolio project for Codecademy's Intro to Programming. The kinds of photography I'm interested in – the kinds that capture people and places, indeed people _in_ places, and that either manage to encapsulate something about a person's essence or to freeze a fascinating, fleeting moment – are profoundly social. They involve putting oneself into proximity with others without putting them off their ease. And, thinking about it from their point of view, there is nothing worse than waiting tensely for someone to fiddle with their camera settings before they finally take your picture. If it's going to be done, it has to be done quickly. Metering for light, selecting shutter speed and aperture, framing the picture, bringing your subject into focus – all this has to happen almost instantaneously, or to has to have been done in advance. Two or three seconds of lens-pointing starts to become noticeable. Beyond five seconds, you're getting unsettling.

Another thing about shooting film is that most available film stocks are designed for daylight. Go indoors, or edge into twilight, and you begin to need the kind of longer exposure times that call for a tripod. I want spontaneity, so that doesn't work for me. The alternative? To use a flash.

When the Nikon F3 was introduced, automatic, through-the-lens (TTL) flash metering, the kind that enables your camera to calculate for you how bright your flash needs to be and automatically control it, was just coming in. The F3 offers TTL in combination with some very specific bits of kit, but for simplicity's sake let's just say they're not an option. Meanwhile, when the Leica IIIF was introduced some three decades earlier, it was still revolutionary for a camera even to be able to sync a flash to the opening of its shutter. So there will be no automatic control here.

If I want to take pictures in dim light, I need to use a flash; and if I want to use a flash, I need to control it manually. To do that, I need to engage in a spot of mental maths.

Every flash has a guide number, or GN, usually given in metres. A GN of 32, for example, means that with the flash at full power, and the camera lens's aperture at f/1, a subject 32m from the camera will be correctly exposed. At f/2, the distance at which a subject will be correctly exposed is 16m; at f/4, 8m, and so on. GN = aperture number * distance; distance = GN / aperture number; aperture number = GN / distance.

The width of your aperture also, however, determines depth of field, or how much of your photograph is in focus and how blurry things get as they come closer or get further away. It's an important creative factor, and you don't necessarily want it to be decided for you by how far away your subject is standing (or vice versa). Flashes like my Nissin i40 therefore also allow the manual photographer to set the power level or intensity of the surge of light produced, in this case from 1/1 down to 1/256. But, to complicate things further, this power level is based on the intensity of light thrown onto a two-dimensional plane. The distance to the subject is linear, i.e. one-dimensional. For every halving of light intensity, therefore, the distance is divided only by the _square root_ of 2. If full power gives a GN of 32m, 1/2 power gives a GN of 23m. Quartering the power halves the distance, to GN 16m. All of these distances also _specifically_ apply at a film sensitivity rating of 100 ISO – ISO being another planar measurement. If you're shooting at 200 ISO, i.e. at twice the sensitivity, then your full-power GN will be multiplied by √2, from 32 to 46. If you're shooting 400 ISO film, as I normally do, it doubles to 64. At 800 ISO, it's approximately 91.

Now, once you've got your head inside this system, there is an intuitive elegance to it. Every time the light is doubled or halved, we say we've gone up or down one stop. Doubling exposure time from 1/1000s to 1/500s increases exposure by one stop. Increasing flash power from 1/16 to 1/8 increases exposure by one stop. Narrowing the aperture from f/5.6 to f/8 decreases exposure by one stop. (Aperture numbers measure the diameter of the aperture as a fraction of the lens's focal length, another linear measurement, so they likewise obey the √2 rule.) And if, in the context of flash photography, your subject moves from 11m to 8m away, or from 2m to 1.4m, you will need to decrease exposure by one stop. Practically speaking, all of these changes involve turning a dial or a ring by exactly one click, from one setting to the next, and knowing whether you should be turning it up or down. All you need to do is first orient yourself in this system of interrelationships – by remembering your flash's GN, multiplying it by the correct exponent of √2 to suit your film's ISO sensitivity, and then, taking a pair of either distance and aperture, aperture and GN or distance and GN, calculating the third variable in that three-way interdependence (along with which flash power level is needed to get the desired GN). Crucially, you need to do this swiftly, before the moment has passed, and you need to do it while simultaneously looking cheerful, confident and relaxed to keep your subject at their ease.

How hard can a bit of Italian _bella figura_ be? When it comes to the achievement of speed, naturalness, and the becoming-one of body, mind and instrument so essential to both, I've always had the same approach: to work nerdily hard behind the scenes. To be, in short, entirely unnatural. Practice makes, if not perfect, then at least better.

We are in the realm of the drill. And, since to drill myself on taking photographs with manual flash I first need to internalize all of these mathematical relationships – and, realistically, probably also to outright memorize certain things, like GNs at certain power levels, or how many stops f/16 is from f/2.8 (the answer is five) – we are in the realm, also, of the flash card.

Photography discursus over. Incipit project nova.

* * *

## PyFlashCards: The Lightweight, Sort-of-Convenient Tool for Creating and Testing Yourself on Flash Card Decks

My chosen project is a terminal-based Python flash card deck. As with real-life flash cards, this isn't going to involve anything close to natural language processing. You're shown side A, and can flip to view the next side when desired. Whether you guessed rightly or wrongly, your performance is self-reported. If it makes you feel better, you can lie, but it'll make the score you get at the end of each round, and then again at the end of the deck, less accurate and thus less useful.

There also needs to be some benefit to using this program rather just buying a stack of cards and writing on them. Here, the major gain can come from the far greater versatility of data. Take, for instance, this table of flash power levels, GNs, distances and resulting apertures at 400 ISO:

![A table showing flash power levels, GNs, distances and resulting apertures at 400 ISO](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pckewtkwa5mi8htlvnxu.png)

Flash cards have two sides. They test the user on their ability to recognise and reproduce an association between, or correspondence of, two pieces of information: 'martyred saint of photography' and 'Oscar Barnack', say, or 'weeks spent on this project' and 'rather more than intended'. Now, there are a lot of ways of breaking that table down into pairs of associated values, a lot of ways you might want to test yourself on it. To do it by hand, you would have to draw up multiple decks by hand. It would involve much duplication of labour and would be incredibly time-consuming. If, however, you were storing your decks in a file format as simple and easy-to-process as CSV, you could very easily take that table and, with the use of a pretty straightforward additional piece of code, digest it into multiple different CSV decks, covering all the different possible combinations, at the press of a button.

So our program needs to load card decks from CSV files. To make that process user-friendly, it should have a loading menu that lists the available CSVs and lets the user select one. If none yet exist, or even if the user just doesn't really want to create their own CSVs, it should also provide its own in-program tool for creating new flash card decks manually, by user input. If it lets the user create decks, it will also have to be able to save them, which in turn means that it should make sure it isn't overwriting existing files in the event that the user gives a file name that's already taken. And, of course, it has to let the user test themself on the currently loaded deck.

![A screenshot of PyFlashCards's ASCII title](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c6hxc9m27z4ja1lhwd4k.png)

The core architecture of the program consists of a set of loops, all of which can be nested one inside the other ad infinitum and still exit by the shortest possible route to the program's end when the user is done. They are the deck creation loop, the deck loading loop, the testing loop and the first we encounter, the main menu loop. All three employ a CardDeck class, which stores a deck title, a 2D list of cards and an integer that tracks the deck's size. This class has methods for adding and removing cards. The core loops are also assisted by a helper loop that handles user inputs with limited possible values, and the helper functions confirm_quit(), which – well; load_deck(), which loads a CSV file's contents into a CardDeck object; title_box(), which spaces out a provided string and draws an ASCII border around it; and file_name_handler(), which checks whether a file name is already in use and, if so, adds a bracketed number to the end (within a specified max filename length) to help us avoid accidental overwriting.

```python
# Cleans up a file name ready for writing, and avoids
# overwriting existing files
def file_name_handler(name, extension, max_len):
    # Strip the filename of illegal characters
    name = re.sub(r"\W+", "", name)

    # If the filename is already taken, add a number to the end
    i = 0
    while isfile(name + "." + extension):
        i += 1
        affix_str = f"({i})"
        # Calculate characters remaining after addition of affix
        new_max_len = max_len – len(affix_str)
        if i > 1:
            # An affix already exists, so take its length
            affix_len = len(name.split("(")[1]) + 1
            # Use it to strip out the old affix
            end_indx = len(name) – affix_len
            name = name[:end_indx]
        # Make sure there's space for the new affix, then add it
        if len(name) > new_max_len:
            name = name[:new_max_len]
        name += affix_str

    # Finally, add the filetype extension and return the result
    return name + "." + extension

```

When PyFlashCards is run, a welcome message is printed and then the main menu loop is called. It checks whether or not any CSVs already exist in the script's directory. If they don't, it prompts the user to make one and calls the deck creation loop. If they do exist, it lets the user choose between LOAD or MAKE. (They can also QUIT or EXIT at this point.) LOAD calls the deck loading loop to get the file number, then gives it to load_deck(). MAKE, unsurprisingly, calls the deck creation loop.

![A gif showing the main menu of PyFlashCards, followed by the first half of a deck's creation](https://media3.giphy.com/media/FpQAAsuSTyG4Npvuzj/giphy.gif)

The deck creation loop first gives some explanatory text, then proceeds card by card to ask the user to enter first side A, then side B. After each pair they can either continue to enter values for the next card, or tell PyFlashCards that they're finished, at which point it'll ask them to give the deck a title. It then prints out the completed deck, and asks the user either to provide a filename for saving or CANCEL; and if the user wishes to CANCEL, it gives them the option to start again or return to the main menu.

![A gif showing the second half of a deck's creation in PyFlashCards](https://media.giphy.com/media/XAxBF0VsRGuYTPsJkF/giphy.gif)

The deck loading loop is passed the file list most recently generated by the main menu loop's checking for files, and prints it as a numbered table in pages of 10 files. Each page declares that it is "Showing files # to # of #". The user can enter the number of their desired file to load it, or press [Enter] to load the next page. Again, they can also CANCEL.

![A gif showing the loading menu in PyFlashCards](https://media.giphy.com/media/8VpI5hrEzL89b7tvpJ/giphy.gif)

The testing loop, finally, works much as you'd expect. The user chooses how many cards they'd like to be shown per round. For each card, side A is shown first. The user can then 'flip' the card to be shown side B, and then must self-report whether or not they got it right. At the end of each round, they're shown their score so far, and have the option either to continue to the next round or stop there. Each card is selected at random from the deck, and is only shown once – achieved by creating a disposable deepcopy of the loaded CardDeck object and using its remove_card() method to pop items from the list one by one as testing proceeds. When the end of the deck is reached, the final score is shown, and the user can either restart the deck or return to the main menu.

![A gif showing a flash card test in PyFlashCards](https://media.giphy.com/media/QTukCuCuijPFw4JIY9/giphy.gif)

I probably picked a more complex project than I was really expected to, here. It has, however, already come in very handy, and I've been using it as designed even during development, pretty much since the loading and testing functions were implemented. It does what it sets out to do, and I'm pleased with it. If I were to return to develop it further, my first task would be to replace the scrolling terminal interface with more of a terminal-style GUI, à la Nano or Vim, with elements presumably being arranged in a buffer before being written out as a full 'screen' that fills the terminal window.

The GitHub repository can be viewed [here](https://github.com/dulle90griet/flash-cards/).

* * *

Programming, incidentally, is another of the teenage passions and interests I've returned to recently. Between the ages of 10 and 18, I taught myself first HTML, XML and CSS, then ActionScript, JavaScript and SQL, then C, C++ and Python. Then I went away. It's fun to be back.