Learn Python by Doing Projects – Part 8: Exceptions, Argparse, and the Vigenère Cipher


Hello! This post is part of my Learn Python by doing projects tutorial series.
In this Python tutorial, you’ll build a command-line encryption tool using the Vigenère cipher. Along the way, you’ll learn how to handle runtime errors with try/except blocks. You will write more robust Python code using exceptions. You will also use argparse to create CLI subcommands.
If you're new to this series, you can start from the beginning.
Now, on to our program!
What you'll learn today
CLI skills: using
argparse
to create subcommandsError handling & exceptions
Using exceptions and try-except blocks to make your program more robust
Error messages, the
error
method, andstderr
The difference between exceptions and errors
How to write encryption and decryption algorithms from scratch with a few functions
The Vigenère cipher
First of all, a cipher is a fancy word meaning an encryption algorithm.
What's the point of cryptography?
Humans are devious creatures. Often, they don't want the whole world to know about their secrets. Since antiquity, we've tried to hide info from our enemies. The point of cryptography is to take a message and to make it unreadable in a deterministic manner.
The right person (and only the right person) should be able to revert it back to its original state.
Cryptography uses algorithms to change the text and revert it. Through the ages, mathematicians have devised lots of different algorithms.
Vigenère
The Vigenère cipher takes a word or a phrase as a key to change the original text's letters. It is part of the category of 'shift ciphers', or 'substitution ciphers'.
The key is a secret template to decide by how many places to shift each letter. Very primitive ciphers used a fixed number to shift all the message's letters (e.g., the Caesar cipher).
Let's work through an example.
Original message: HELLO
Key : CRYPTO
Let's first convert each letter to a number (e.g., by taking its place in the alphabet, starting at 0).
H, E, L, L, O -> 7, 4, 11, 11, 14
C, R, Y, P, T, O -> 2, 17, 24, 15, 19, 14
Then, add the numbers.
H (7) + C (2) = 9
E (4) + R (17) = 21
L (11) + Y (24) = 35
L (11) + P (15) = 26
O (14) + T (19) = 33
Some numbers are higher than 25 (letter Z). Apply a modulo 26 to wrap them around to A.
9 -> J
21 -> V
35 mod 26 = 9 -> J
26 mod 26 = 0 -> A
33 mod 26 = 7 -> H
After encryption, our message is 'JVJAH'.
You see that both 'H' and the first 'L' become 'J'. Different letters can become the same encrypted character. Both 'L's become different characters: 'J' and 'A'. This shows that the Vigenère cipher is not a straightforward substitution algorithm.
What happens if the key is shorter than the message? Then you repeat the key over and over. For example:
Message: SUPERCALIFRAGILISTICEXPIALIDOCIOUS
Key: KEY
You would expand the key like this.
SUPERCALIFRAGILISTICEXPIALIDOCIOUS
KEYKEYKEYKEYKEYKEYKEYKEYKEYKEYKEYK
The Vigenère algorithm was very secure long ago. Nowadays, it's easy for a computer to break it and my next post will cover that. But first, let's learn how to encrypt and decrypt!
The program's architecture
I have decided to divide the program according to functionality:
Command line arguments with the
argparse
moduleFile I/O
The Vigenère algorithm proper.
I want to call the program from the command line. I would like to write something like this:
% python3 vigenere.py encrypt mykey origin_file.txt
Or still,
% python3 vigenere.py decrypt mykey crypted_file.txt
We could also store results in a file, like this.
% python3 vigenere.py -o output_file.txt encrypt mykey origin_file.txt
Subcommands with argparse
We will use the argparse
module to capture arguments from the command line. If you don't know the basics of using the argparse
module or if you want a refresher, check out this post first.
As always, start by creating a parser and add an optional argument for the output file.
parser = argparse.ArgumentParser(description="Vigenere cryptography")
parser.add_argument("-o", "--output",
default=None,
help="where to write the result")
Now, we have to deal with arguments for encryption, decryption, and cracking. It would be straightforward to add two arguments: a key and an input file. That would make sense, but by definition, cracking doesn't need any key. Enter subcommands: we’ll give encrypt
, decrypt
and crack
each their own parser so that only crack
can omit a key.
Create subcommands by calling the add_subparsers
method on the main parser.
The result of the add_subparsers
method is a special kind of object. It's sole use is that you can attach smaller parsers to it. You use those little subparsers as you would a normal parser. Here's the workflow to using subcommands.
Call the
add_subparsers
methodAdd subparsers using the
add_parser
methodAdd arguments to the subparsers with
add_argument
Here's how to add the encrypt command.
subparsers = parser.add_subparsers(dest="command", required=True)
We only call the add_subparsers
method once, for all three commands.
To add the encrypt
, decrypt
, and crack
commands, add parsers to the subparsers object.
enc = subparsers.add_parser("encrypt",
help="Encrypt a text with a secret key")
enc.add_argument("key", help="secret key to use")
enc.add_argument("origin", nargs="?",
default=None,
help="file to encrypt")
The add_argument method can take lots of different arguments. Here, "key"
and "origin"
are the argument names.
nargs
is an optional keyword argument which tells how many arguments to consume. The"?"
meansorigin
takes one argument if possible, zero otherwise. This is logical, since the user either specifies an origin filename, or doesn't.default
is an optional keyword argument. It tells what value to use if the user doesn't give a filename.I added a
help
string, which will end up in the program's help page.
The add_argument method takes many more optional arguments, making it flexible. You can read about them here.
To add other commands, call the add_parser
on the subparsers object.
dec = subparsers.add_parser("decrypt",
help="Decrypt a text with a secret key")
dec.add_argument("key", help="secret key to use")
dec.add_argument("origin", nargs='?',
default=None,
help="string to decrypt")
crack = subparsers.add_parser("crack",
help="Crack a code without a secret key")
crack.add_argument("origin", nargs='?',
default=None,
help="string to decrypt")
As you can see, this can get repetitive once you get the hang of it.
Now, call parse_args
on the parser
object.
args = parser.parse_args()
Automatic help page generation
To recap, we call our program with a choice of three commands:
encrypt
decrypt
crack
One neat feature of argparse
is that it can generate help pages without us doing any more work.
Let's assume you called your program's file vigenere.py
. You can check out this functionality by typing these commands.
% python3 vigenere.py -h
usage: project8.py [-h] [-o OUTPUT] {encrypt,decrypt,crack} ...
Vigenere cryptography
positional arguments:
{encrypt,decrypt,crack}
encrypt Encrypt a text with a secret key
decrypt Decrypt a text with a secret key
crack Crack a code without a secret key
options:
-h, --help show this help message and exit
-o OUTPUT, --output OUTPUT
where to write the result
Each subcommand has its own -h
page.
% python3 viginere.py encrypt -h
In the help pages, square brackets denote optional arguments.
You can add a print expression at the end of your program to check the value of args with different inputs.
print(args)
argparse wrap-up
Let's wrap up the command line code in a function.
def get_args():
"""
Parse and return command-line arguments for the Vigenere tool.
Sets up a global -o/--output option and subcommands:
encrypt, decrypt, and crack, each with their own required
arguments. Performs key validation for encrypt and decrypt.
"""
parser = argparse.ArgumentParser(description="Vigenere cryptography")
parser.add_argument("-o", "--output",
default=None,
help="where to write the result")
subparsers = parser.add_subparsers(dest="command", required=True)
enc = subparsers.add_parser("encrypt",
help="Encrypt a text with a secret key")
enc.add_argument("key", help="secret key to use")
enc.add_argument("origin", nargs="?",
default=None,
help="file to encrypt")
dec = subparsers.add_parser("decrypt",
help="Decrypt a text with a secret key")
dec.add_argument("key", help="secret key to use")
dec.add_argument("origin", nargs='?',
default=None,
help="string to decrypt")
crack = subparsers.add_parser("crack",
help="Crack a code without a secret key")
crack.add_argument("origin", nargs='?',
default=None,
help="string to decrypt")
args = parser.parse_args()
return args
Robust file input and output (I/O)
Two functions will handle I/O:
get_origin
will handle the input side. If the user provided a filename, try to read it. Otherwise, ask the user to enter the input by hand.output_results
will handle the output. If the user provided a file name, write to it. Otherwise, print the results to the screen.
Here are get_origin
and output_results
. Read them first and try to understand the code the best you can.
def get_origin(args):
"""
Load the origin text from a file or prompt the user.
If args.origin is a filename, attempts to read it; otherwise, falls
back to input(). Exits with an error message on file I/O failure.
"""
if args.origin:
try:
with open(args.origin) as f:
origin = f.read()
except OSError as e:
sys.exit(f"Error reading {args.origin!r}: {e}")
else:
try:
origin = input("Enter text: ")
except EOFError as e:
sys.exit(f"Error reading from stdin: {e}")
return origin
def output_results(args, results):
"""
Write results to a file or print them to standard output.
If args.output is specified, writes results there; otherwise,
prints to the console. Exits with an error on file I/O failure.
"""
if args.output:
try:
with open(args.output, mode='w') as f:
f.write(results)
except OSError as e:
print(results)
sys.exit(f"Error writing {args.output!r}: {e}")
else:
print(results)
What are Exceptions in Python?
In both functions, I used try/except
statements. They are one way Python programmers deal with exceptions.
Exceptions are objects that signal an unexpected problem during program execution. As their name implies, they should be exceptional. Exceptions interrupt the program's flow to allow you to deal with them.
An analogy would be a cake recipe. The recipe controls the 'normal' steps to make a cake. Take the ingredients, measure them, mix them, bake the cake. Now let's say you drop all your eggs mid-recipe and break them. That's not supposed to happen and is an exceptional occurrence. If you want to finish your cake, you'll have to stop what you're doing and go buy some new eggs.
In this analogy, the recipe is the normal program flow. Breaking the eggs is an exception. Buying new eggs then resuming your baking is handling the exception.
Python comes with built-in exceptions. While it is possible to define your own, I won't cover that in this post. Indeed, built-in exceptions cover most of a beginner's needs.
Here are some useful built-in Python exceptions that you can already use.
EOFError
. The input function for when it hits an end-of-file character (EOF
) without reading any data. It means the function has received nothing.IndexError
. Use it when your program tries to access a sequence element with an out-of bounds index. For example, if the listmy_list
has four elements,my_list[10]
will give an IndexError.KeyError
. Use it when your program tries to access a dictionary's key that doesn't exist.MemoryError
. Use this when your program runs out of memory.OSError
. Sometimes, exceptions come not from within your program, but from the operating system. For example, if the OS can't open a file for you, or if it doesn't find it.TypeError
. This happens when a function receives an object of the wrong type. For example, if you try to add a number and a string, there'll be aTypeError
.ValueError
. This exceptions happens when a part of your code receives an object of the right type, but of the wrong value.
You can find the official list of built-in exceptions here.
In the code, I envisioned two possible exceptions:
A problem occurring when reading or writing to files:
OSError
.A problem occurring when receiving no user input:
EOFError
.
Python Exception Handling with try/except
One way to use exceptions is to insert them in try/except
blocks. They have this basic structure:
try:
<block of code>
except <exception class> as <exception_name>:
<handle exception_name>
You read that as "Try to execute this block of code. If it raises this particular class of exceptions, handle it this way."
try:
with open(args.origin) as f:
origin = f.read()
except OSError as e:
sys.exit(f"Error reading {args.origin!r}: {e}")
This piece of code reads like this: "Try to open the origin file and read its contents. If there's a problem caused by the operating system, call it "e" and display it while exiting the program".
At the end of the program's execution, the output_results
function mirrors this structure.
try:
with open(args.output, mode='w') as f:
f.write(results)
except OSError as e:
print(results)
sys.exit(f"Error writing {args.output!r}: {e}")
Back to get_origin
, we can now tackle the last bit of exception handling in our program. Try to make sense of it by yourself first.
try:
origin = input("Enter text: ")
except EOFError as e:
sys.exit(f"Error reading from stdin: {e}")
This reads like this: "Try to get input from the user. If you get an end-of-file rather than legitimate data, end the program with an error message."
Writing the cryptographic functions
By this point, we still need to write the meat of the program: the actual cryptographic functions.
First, prepare the key
to match the origin input's length.
def prepare_key(key, text_len):
"""
Repeat the key to match the length of the input text.
Given a key string and the desired text_len, returns a new string
formed by repeating key as many times as needed and then truncating
to exactly text_len.
e.g.: key: "I love Python."
"This string is the origin text."
"I love Python.I love Python. I "
"""
expanded_key = (key * (text_len//len(key) + 1))[:text_len]
return expanded_key
First, create a longer string than needed (key * (text_len//len(key) + 1)
). Then, slice it to the desired length ([:text_len]
).
Notice how the *
operator repeats a string.
Now onto encrypting and decrypting. The first thing to realize is that those two operations are very similar. Loop through the text, character by character, and apply a shift function.
At this point we need to think about the character set
we'll use, because that will condition what we do next. For example, classic Vigenère cipher implementations only encrypt alphabetic characters. Numbers, punctuation and whitespace stay as is. In that case, "Hello, world! 123" would become "Zincs, pgvnu! 123" with the key "secret".
I have decided to use a custom character set, including most useful characters. This will include whitespace. Store it in a ALPHABET
constant.
ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890#@&\"\'(!)-_*$%+=:/;.,?<>[]{}\n\t\\ éàçèù^¨`°"
To make my life easier, I create two other constants:
the alphabet's length
a dictionary of characters and their indexes.
ALPHA_LEN = len(ALPHABET)
INDEX = {c: i for i, c in enumerate(ALPHABET)}
If you forgot about the enumerate
function, you can read about it back here or at the official Python docs.
Next, we can tackle looping through a string to shift each character.
def crypt_iter(origin, expanded_key, crypt_fun):
"""
Apply a shift function to each character in the origin text.
Iterates over origin, and for each character that exists in the
ALPHABET, applies crypt_fun to its index and the corresponding
key character’s index. Characters not in the alphabet are passed through
unchanged.
"""
dest = []
for i, ch in enumerate(origin):
if ch in INDEX:
a = INDEX[ch]
k = INDEX[expanded_key[i]]
res_num = crypt_fun(a, k)
res_letter = ALPHABET[res_num]
dest.append(res_letter)
else:
dest.append(ch)
return ''.join(dest)
The function takes as argument
the string to transform
an "expanded key", which is a key that went through the
prepare_key
functionand a cryptographic function (how you shift the character). This is the difference between encryption and decryption.
This function
goes through each character in origin
checks if it is part of the
ALPHABET
converts it to a number
converts the corresponding key character to a number
calls the cryptographic function
converts the result back to a character
puts everything back into a string.
Here is another version, changing only lowercase letter characters. It would have been simpler. Let's assume we did the necessary adjustments for an all-lowercase origin
and key
. The key would only contain letters, too.
dest = []
for i, ch in enumerate(origin):
if ch.isalpha():
a = ord(ch)
k = ord(expanded_key[i])
res_num = crypt_fun(a, k)
res_letter = chr(res_num)
dest.append(res_letter)
else:
dest.append(ch)
return ''.join(dest)
Can you guess what ord
and chr
do? Here are links to their official docs: ord, chr.
Now, I write the encryption/decryption at once, in three functions: crypto
, encrypt
and decrypt
.
def crypto(origin, key, crypt_fun):
"""
Core Vigenère routine: expand key and transform the origin text.
1. Computes expanded_key to align with origin length.
2. Calls crypt_iter to perform the per-character shifts.
"""
text_len = len(origin)
expanded_key = prepare_key(key, text_len)
result = crypt_iter(origin,
expanded_key,
crypt_fun)
return result
def encrypt(origin, key):
"""
Encrypt the origin text using the Vigenere cipher.
Applies a modular addition shift (plaintext + key) over the full
ALPHABET, returning the resulting ciphertext.
"""
encrypted = crypto(origin,
key,
lambda p, k: (p + k) % ALPHA_LEN)
return encrypted
def decrypt(origin, key):
"""
Decrypt a Vigenere-encrypted text using the provided key.
Applies a modular subtraction shift (ciphertext - key) over the full
ALPHABET, returning the recovered plaintext.
"""
decrypted = crypto(origin,
key,
lambda c, k: (c - k + ALPHA_LEN) % ALPHA_LEN)
return decrypted
The only difference between encrypt and decrypt is one little function
encrypt:
lambda p, k: (p + k) % ALPHA_LEN)
decrypt:
lambda c, k: (c - k + ALPHA_LEN) % ALPHA_LEN)
Correcting a bug: Errors
There's a little bug in our code. What if the user enters an empty string as a key? Right now, get_args
will populate args.key
with an empty string. So far so good, right?
Except the prepare_key
function divides by the length of the key: (key * (text_len//len(key) + 1))[:text_len]
. And we can't divide by 0!
The user should be able to correct that right at the end of get_args
. You can add this snippet of code just before the function returns.
if args.command in ("encrypt", "decrypt"):
if not args.key.strip():
parser.error(f"Key cannot be empty or only whitespace.")
for ch in args.key:
if ch not in ALPHABET:
parser.error(f"Invalid character in key: {ch!r}")
There's a new concept here: error messages and the standard error. It appears in parser.error("...")
. We could print the error messages using print
. This is not common practice for a few reasons.
error messages go to
stderr
(standard error). The print function sends messages tostdout
(standard output).Sometimes you want to redirect all program output to a file (in Linux or Mac OS, you would do, e.g.,
my_program > output.txt
). You don't want potential errors polluting the file. Those errors will keep appearing to the screen for debugging.You might want to do the opposite: redirecting errors to a file and printing the normal output to the screen. That's how you log a system's errors.
stderr
andstdout
are separate and you can read them in parallel. For example, a function monitoring errors shouldn't go through all the non-error data.
Errors vs exceptions
This brings a question: what's the difference between errors and exceptions?
After all, built-in exceptions have names like OSError
, TypeError
, etc.
The official Python documentation is unclear on the distinction between the two.
On one hand, errors are subclasses of the BaseException class. This means that errors are implemented as exceptions.
But the Python docs contradict themselves by stating that exceptions are a type of error. In fact, there are two types of errors:
exceptions
syntax errors
The difference between the two is when the error happens. Syntax errors happen at parse time, when the interpreter tries to make sense of your program. Exceptions happen at run time, when the interpreter executes the program.
For example, a missing parenthesis will generate a syntax error. Python will catch it before it tries to run the expression.
Trying to open a file that doesn't exist will generate a runtime exception. The Python interpreter has to try to open the file before realizing it's impossible.
This is confusing so I will give you a more pragmatic way to think about errors and exceptions.
Example of errors are
Referencing a variable that doesn't exist. Error names are
NameError
, or the more specificUnboundLocalError
for local variables.Improper indentation:
IndentationError
, or the more specificTabError
.Importing a function that doesn't exist:
ImportError
or the more specificModuleNotFoundError
.
You should correct them as you're writing the code, by testing each function as soon as you've written it. Now imagine if someone tried catching these errors at runtime...
try:
try:
<import a bunch of modules>
try:
<rest of program>
except Exception as catch_all_e:
print(f"What could go wrong??? {catch_all_e}")
except ModuleNotFoundError as module_e:
print(f" Modules dreamt up in the throes of a fever nightmare: {module_e}")
except SyntaxError as distracted_e:
print(f"The programmer hasn't bothered debugging his terrible code: {distracted_e}")
Now let's say your program needs to download a file, open it and read its contents. For some reason your internet connection had a problem and you end up with mangled data. That's not a bug, but you need to plan for it. It's also a rare occurrence. Some might say... an exception?
Concept | Purpose | Syntax Example |
Error | Serious issue, often uncatchable | SyntaxError , MemoryError |
Exception | Runtime issue, can be handled | ZeroDividionError , ValueError |
try/except | Catch and handle exceptions | try: risky(); except Error: |
Exceptions are part of writing resilient programs. Errors are signals that your code still needs debugging.
What you learned
In this project, you practiced:
Writing robust Python code using
try/except
blocksCatching and handling file I/O and user input exceptions
Using argparse with subcommands to build a CLI interface
Creating an encryption and decryption system using the Vigenère cipher
Differentiating between errors and exceptions in Python
You now have a much richer skillset to create robust Python tools!
Recap: argparse subcommands, exceptions, try/except blocks, errors
Argparse subcommands
Splitting the CLI: We broke the tool into three distinct modes: encrypt, decrypt, and crack. Each command has its own options and arguments.
Creating subparsers: The main parser dispatches each command to a smaller parser. Generate the subcommand scaffolding in two steps.
subparsers = parser.add_subparsers(dest="command", required=True)
enc = subparsers.add_parser("encrypt", help="…")
Defining arguments per command: Each subparser is like an independent ArgumentParser
. Add arguments to a subparser with add_argument
.
enc.add_argument("key", help="secret key to use")
enc.add_argument("origin", nargs="?", help="file to encrypt")
Dispatching: At the end, you call args = parser.parse_args()
. argparse
inspects the first positional token (encrypt
, decrypt
, or crack
). Then, it parses only that subparser’s arguments into args
.
Automatic help pages: You get -h/--help
for the main program and each subcommand for free.
$ python vigenere.py encrypt -h
usage: vigenere.py encrypt [-h] key [origin]
Error messages and stderr
We caught a bug where a user could provide an invalid cryptographic key. This would crash the program down the line. Rather than dealing with this issue when it's too late, I decided to catch it at argument parse time.
Calling parser.error("…")
prints your message to stderr
and exits the program.
stderr
is like stdout
, but for errors instead of standard output. Separating normal output (stdout
) from error messages (stderr
) has a slew of advantages. For example, you might want to write logging functions to monitor errors. This function would need to access your error messages, but not your standard output. The inverse is saving your output to a file. You don't want error messages to pollute it.
Exceptions & the try-except pattern
Sometimes, correct code might fail. Exceptions capture those failures and let you deal with them as you see fit.
We used the common try/except pattern in our code. Here is its basic form.
try:
result = risky_operation()
except SpecificException as e:
handle_exception_code(e)
This structure isolates the failure-prone code.
You could use a bare except:
statement instead of the except SpecificException as e:
. This is not good practice. Always try to catch the narrowest exceptions as possible. This way you won't swallow and miss unexpected bugs by accident.
Exceptions vs errors
Syntax errors occur at parse time. They prevent your code from running at all (e.g. missing colon, mismatched parentheses).
Exceptions occur at runtime. They signal conditions you can either recover from or report. For example, trying to open a file that's not there raises OSError
.
Pragmatic distinction:
Errors (like
SyntaxError
,IndentationError
) mean your code has bugs. You must fix it.Exceptions (like
ValueError
,FileNotFoundError
) mean your code encountered an unexpected situation during execution.
Exercises
Easy
Encrypt a message: Use the encrypt command to encrypt the message "HELLO" with the key "KEY". Call our program from the command line.
Decrypt the message and confirm it matches. Use the output from the previous step. Try decrypting it using the same key and confirm that you get "HELLO" back.
Encrypt from a file, output to another file. Create a text file message.txt with some text. Encrypt it using a key and save the result in encrypted.txt.
Trigger a missing input file error. Run encrypt or decrypt on a nonexistent file, like ghost.txt. Observe the OSError and the clean error message.
Try an empty key. Call
encrypt "" message.txt
. Verify that it fails with a key-related error from parser.error.Manually input text (no file). Run the tool without specifying an input file. When prompted, type in a message. Try with and without EOF (e.g. Ctrl+D) to test exception handling.
Use an invalid character in the key. Try a key like abc🙂xyz and confirm that the validation code rejects it with a useful message.
Experiment with --output vs. no --output. Encrypt or decrypt once with
-o output.txt
, once without. Check that one writes to a file, and one prints to console.Use encrypt and decrypt subcommand help pages. Try
python3 vigenere.py encrypt -h
anddecrypt -h
. Explore what each argument does.Encrypt multiline input. Use a file containing complex data, for example, the program's source code. Encrypt it, and ensure the formatting is the same in the result.
Medium
Add a --force overwrite flag. Change the program to refuse overwriting an output file unless the user passes a
--force
flag. Raise a custom message if the file already exists without the flag. A useful function isos.path.exists()
; you'll have to import theos
module. Use an exception (a simpleException
will do the trick) if the file exists and the user provided no force flag.Add a --strict flag that restricts unknown characters. Right now, unknown characters pass through crypt_iter without change. Add a
--strict
flag: if it’s set, raise an exception (ValueError
) when encountering any character not inALPHABET
.Support lowercase-only alphabet mode. Add an option
--mode=full|lowercase
. The user would type--mode=lowercase
to toggle it or--mode=full
to prevent it. The default should be--mode=full
. If he chooses lowercase, restrict ALPHABET to "abcdefghijklmnopqrstuvwxyz" and adjust validation.Add --keyfile option to load the key from a file. Users can pass
--keyfile path.txt
instead of typing the key. Confirm that the file has a single non-empty line, and contains valid key characters. Use a try-except block to open the file.
Hard
Add a --dry-run option. Change the code so that, if you pass it
--dry-run
, the result is printed but never saved to a file, even if you specify--output
.Refactor encrypt and decrypt to accept any shift function. Allow the user to pass in a
--func
argument to pass cryptographic lambda functions.Display readable error messages with color (optional). Use
sys.stderr.write()
with ANSI color codes to make error messages more noticeable (e.g. red text). You will have to import thesys
module. Bonus: only use colors ifsys.stderr.isatty()
is True. It is a good exercise for you to read through and make sense of unfamiliar documentation. (Hint: look forsys.stderr
…)
That’s it for today. I hope you enjoyed this post. If so, stay tuned because the next one will be about cracking the Vigenère cipher.
Subscribe to my newsletter
Read articles from Had Will directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
