Exceptions and Files Answers
Exception Exercises
Hint
If you get stuck for a minute or more, try searching Google or using help.
If you’re stuck for more than a few minutes, some of these links might be helpful for some of the exercises below:
Length or None
This is the len_or_none exercise in exception.py.
Write a function len_or_none that returns the length of a given object or None if the object has no length.
>>> from exception import len_or_none
>>> len_or_none("hello")
5
>>> len(4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: object of type 'int' has no len()
>>> len_or_none(4)
>>> print(len_or_none(4))
None
>>> len_or_none([])
0
>>> len_or_none(zip([1, 2], [3, 4]))
>>> print(len_or_none(zip([1, 2], [3, 4])))
None
>>> len_or_none(range(10))
10
Answers
def len_or_none(var):
"""Return length of object or ``None`` if object has no length."""
try:
return len(var)
except TypeError:
return None
Average
This is the average.py exercise in the modules directory. Create the file average.py in the modules sub-directory of the exercises directory. To test it, run python test.py average.py from your exercises directory.
Make a program average.py that calculates the average of all provided command-line arguments and prints an error message if no arguments are provided and a second error message if invalid arguments are provided.
Note
To test these changes, you should modify the AverageTests class in modules_test.py to comment out the unittest.skip lines from the appropriate test methods.
$ python average.py
No numbers to average!
$ python average.py 2 3 4 5 6 7
Average is 4.5
$ python average.py 2 3 4
Average is 3.0
$ python average.py 2 s 3
Invalid values entered, only numbers allowed!
Answers
import sys
arguments = sys.argv[1:]
if len(arguments) == 0:
sys.exit("No numbers to average!")
total = 0
for n in arguments:
try:
total += float(n)
except ValueError:
sys.exit("Invalid values entered, only numbers allowed!")
print(f"Average is {total/len(arguments)}")
Flipped Dictionary Exception
Modify the flip_dict exercise in dictionaries.py to disallow duplicate values in the given dictionary.
A ValueError should be raised if a dictionary with duplicate values is provided.
Note
To run tests for this updated program, open dictionaries_test.py, find the line that starts with class FlipDictTests. Look for the test named test_with_collisions and comment out the @unittest.skip line just above it. That line tells the Test Framework to skip the test because we don’t expect the test to pass, but now we are changing the program so the test should pass, therefore we comment the line out.
Answers
def flip_dict(old_dictionary):
"""Return a new dictionary that maps the original values to the keys."""
new_dictionary = {
value: key
for key, value in old_dictionary.items()
}
if len(old_dictionary) != len(new_dictionary):
raise ValueError("Duplicate dictionary values found")
return new_dictionary
def flip_dict(dictionary):
"""Return a new dictionary that maps the original values to the keys."""
if len(set(dictionary.values())) != len(dictionary):
raise ValueError("Duplicate dictionary values found")
return {
value: key
for key, value in dictionary.items()
}
Negative Factors
Modify the get_factors exercise in ranges.py to raise a ValueError for negative numbers.
The exception message should state Only positive numbers are supported.
Note
To run tests for this updated program, open ranges_test.py and look for the line with “Comment this line for negative factors exercise” in it, and comment out that @skip line. Previously, the code we wrote wouldn’t pass the test, but now we are changing the program so the test should pass, therefore we comment the line out.
Answers
def get_factors(number):
"""Return a list of all factors of the given number."""
if number < 0:
raise ValueError("Only positive numbers are supported")
factors = []
for x in range(1, number + 1):
if number % x == 0:
factors.append(x)
return factors
Deep Add
This is the deep_add exercise in exception.py.
Write a function deep_add that sums up all values given to it, including summing up the values of any contained collections.
>>> from exception import deep_add
>>> deep_add([1, 2, 3, 4])
10
>>> deep_add([(1, 2), [3, {4, 5}]])
15
Answers
With a counter variable:
def deep_add(iterable_or_number):
"""Return sum of values in given iterable, iterating deeply."""
total = 0
try:
for x in iterable_or_number:
total += deep_add(x)
except TypeError:
total += iterable_or_number
return total
CLI Only Error
Edit the hello.py file in the modules directory that you created in the “Modules->Modules Exercises” section.
To test it, run python test.py hello.py in your exercises directory.
Modify the hello.py exercise to raise an ImportError when the module is imported.
The exception message should state This module can only be run from the command-line.
Note
To run tests for this updated program, open modules_test.py, find the line that starts with class HelloTests. Look for the test named test_exception_on_import and comment out the @unittest.skip line just above it. That line tells the Test Framework to skip the test because we don’t expect the test to pass, but now we are changing the program so the test should pass, therefore we comment the line out. In addition, un-comment the @unittest.skip line just above the test named test_import.
Answers
if __name__ == "__main__":
print("Hello world!")
else:
raise ImportError("This module can only be run from the command-line")
Deep Flatten
This is the deep_flatten exercise in exception.py.
Write a function deep_flatten that flattens all items given to it, including any contained collections, returning a list of all elements. As a bonus, make it work for strings, too.
>>> from exception import deep_flatten
>>> deep_flatten([1, 2, 3, 4])
[1, 2, 3, 4]
>>> deep_flatten([[1, 2], [3, [4, 5]]])
[1, 2, 3, 4, 5]
Bonus version should work like this:
Note
To test the bonus, you should modify the DeepFlattenTests class in exception_test.py to comment out the unittest.skip line from the appropriate test method.
>>> from exception import deep_flatten
>>> deep_flatten(["Hello ", "World"])
['Hello ', 'World']
>>> deep_flatten([(1, 2), [3, "bye", {4, 5}]])
[1, 2, 3, 'bye', 4, 5]
Answers
Naive version - works for the given inputs, but will not work for string input, because a single character will not generate the error we are depending upon:
def deep_flatten(iterable_or_item):
"""Return flattened version of given iterable of iterables."""
result = []
try:
for element in iterable_or_item:
result.extend(deep_flatten(element))
except TypeError:
result.append(iterable_or_item)
return result
Bonus version that doesn’t break for strings:
def deep_flatten(iterable_or_item):
"""Return flattened version of given iterable of iterables."""
result = []
if isinstance(iterable_or_item, (str, bytes)):
result.append(iterable_or_item)
else:
try:
for element in iterable_or_item:
result.extend(deep_flatten(element))
except TypeError:
result.append(iterable_or_item)
return result
Exit Twice
This is the countdown.py exercise in the modules directory. Create the file countdown.py in the modules sub-directory of the exercises directory. To test it, run python test.py countdown.py from your exercises directory.
Write a program countdown.py that counts down from a given number, pausing for 1 seconds in between each number. The program should only exit if Ctrl-C is pressed twice.
You can pause for 1 second by using sleep from the time module.
Ctrl-C pressed just once:
$ python countdown.py 5
5
4
^C Press Ctrl-C again to exit
3
2
1
Ctrl-C pressed twice:
$ python countdown.py 10
10
9
^C Press Ctrl-C again to exit
8
7
^C Goodbye!
Answers
import sys
from time import sleep
def countdown(pause):
interrupted = False
for i in range(pause, 0, -1):
print(i)
try:
sleep(1)
except KeyboardInterrupt:
if interrupted:
print("\nGoodbye!")
exit(0)
else:
interrupted = True
print("\nPress Ctrl-C again to exit")
if __name__ == "__main__":
countdown(int(sys.argv[1]))
CSV Exercises
Note
If you are using a Windows machine, you might have issues with double-spacing of lines. If so, add newline='' to your file open() statement when opening the files for writing.
Total Air Travel
This is the total_air_travel.py exercise in the modules directory. Create the file total_air_travel.py in the modules sub-directory of the exercises directory. To test it, run python test.py total_air_travel.py from your exercises directory.
To test this manually, use the file expenses.csv.
Given a CSV file containing expenses by category, I’d like you to calculate how much money was spent on the category “Air Travel”.
The file is formatted like this:
Date,Merchant,Cost,Category
1/05/2017,American Airlines,519.25,Air Travel
1/12/2017,Southwest Airlines,298.90,Air Travel
1/17/2017,Mailchimp,19.80,Software
2/01/2017,Zapier,15.00,Software
2/05/2017,Lyft,24.24,Ground Transport
2/06/2017,Hattie Bs,18.13,Food
2/06/2017,Lyft,15.65,Ground Transport
The columns are:
The date of the expense
Merchant
Amount paid
Category
Your program should accept a single CSV file as input and it should print a floating point number representing the sum of the amounts paid for all “Air Travel” category expenses.
To test it manually, cd to the modules directory and run:
$ python total_air_travel.py expenses.csv
818.15
Answers
With csv.reader:
import csv
import sys
csv_filename = sys.argv[1]
total = 0
with open(csv_filename) as csv_file:
reader = csv.reader(csv_file)
next(reader) # Skip headers
for date, merchant, cost, category in reader:
if category == 'Air Travel':
total += float(cost)
print(total)
With csv.DictReader:
import csv
import sys
csv_filename = sys.argv[1]
total = 0
with open(csv_filename) as csv_file:
reader = csv.DictReader(csv_file)
for row in reader:
if row['Category'] == 'Air Travel':
total += float(row['Cost'])
print(total)
Pipe to Comma
This is the pipe_to_comma.py exercise in the modules directory. Create the file pipe_to_comma.py in the modules sub-directory of the exercises directory. To test it, run python test.py pipe_to_comma.py from your exercises directory.
Write a program pipe_to_comma.py that reads a pipe-delimited CSV file and converts it to a comma-delimited CSV file.
The pipe symbol is |.
$ python pipe_to_comma.py input_file.psv output_file.csv
The file output_file will contain the data from input_file, with the delimiter changed to commas.
Answers
import csv
import sys
def pipe_to_comma(in_filename, out_filename):
"""Read pipe-delimited file and write CSV file back out."""
with open(in_filename, mode='rt') as in_file:
rows = [row for row in csv.reader(in_file, delimiter="|")]
with open(out_filename, mode='wt', newline='') as out_file:
csv.writer(out_file, delimiter=",").writerows(rows)
if __name__ == "__main__":
pipe_to_comma(*sys.argv[1:])
Ungroup
This is the ungroup.py exercise in the modules directory. Create the file ungroup.py in the modules sub-directory of the exercises directory. To test it, run python test.py ungroup.py from your exercises directory.
Write a program ungroup.py that reads a CSV file that has some empty fields in its first column and fills those fields with the last value from the first column.
If this program is run like this:
$ python ungroup.py input_file.csv output_file.csv
And input_file.csv contains this:
Category,Item,Expense
Swag,rubber ducks,$50
,stickers,$150
,balloons,$40
Food,breakfast,$300
,lunch,$600
,coffee,$130
Then output_file.csv will be written with these contents:
Category,Item,Expense
Swag,rubber ducks,$50
Swag,stickers,$150
Swag,balloons,$40
Food,breakfast,$300
Food,lunch,$600
Food,coffee,$130
Answers
import csv
import sys
[in_filename, out_filename] = sys.argv[1:]
with open(in_filename, mode="rt", newline="") as csv_file:
reader = csv.reader(csv_file)
rows = list(reader)
group = None
for row in rows:
if row[0] == "" and group:
row[0] = group
else:
group = row[0]
with open(out_filename, mode="wt", newline="") as csv_file:
writer = csv.writer(csv_file)
writer.writerows(rows)
Sort By Column
This is the sort_by_column.py exercise in the modules directory. Create the file sort_by_column.py in the modules sub-directory of the exercises directory. To test it, run python test.py sort_by_column.py from your exercises directory.
The program sort_by_column.py takes a filename of a CSV file as input, plus the output file name and a number representing the column to sort. It reads the CSV file, sorts the file by the given column (starting at 0 since we’re computer people), and writes the file to the output file.
Assume the CSV file has 1 single header row.
$ python sort_by_column.py in_filename out_filename 0
The file file_name will be modified so the second column is sorted.
Answers
import csv
import sys
in_filename, out_filename, column_number = sys.argv[1:]
n = int(column_number)
def get_column_n(row): return row[n]
with open(in_filename) as in_file:
reader = csv.reader(in_file)
header = next(reader)
sorted_rows = sorted(reader, key=get_column_n)
with open(out_filename,
mode='w', encoding='utf-8', newline='') as out_file:
writer = csv.writer(out_file)
writer.writerow(header)
writer.writerows(sorted_rows)
Re-order
This is the reorder.py exercise in the modules directory. Create the file reorder.py in the modules sub-directory of the exercises directory. To test it, run python test.py reorder.py from your exercises directory.
Create a program reorder.py that takes an input file and output file names as input on the command line. It should read a CSV file, swap the first and second columns, and write the CSV back out to the new file.
$ python reorder.py input_file_name output_file_name
The file output_file_name will contain the data from input_file_name, with the columns swapped.
Answers
With DictReader/DictWriter:
import csv
import sys
def re_order(in_filename, out_filename):
"""Read CSV file, swap first two columns, and output new file."""
with open(in_filename) as csv_file:
reader = csv.DictReader(csv_file)
rows = list(reader)
first, second, *rest = reader.fieldnames
headers = [second, first] + rest
with open(out_filename, mode='wt', newline='') as csv_file:
writer = csv.DictWriter(csv_file, fieldnames=headers)
writer.writeheader()
writer.writerows(rows)
if __name__ == "__main__":
re_order(*sys.argv[1:])
Without DictReader/DictWriter (possibly headerless files):
import csv
import sys
def re_order(in_filename, out_filename):
"""Read CSV file, swap first two columns, and output new file."""
with open(in_filename, mode='rt') as in_file:
reordered_rows = [
[second, first] + remainder
for (first, second, *remainder)
in csv.reader(in_file)
]
with open(out_filename, mode='wt', newline='') as out_file:
csv.writer(out_file).writerows(reordered_rows)
if __name__ == "__main__":
re_order(*sys.argv[1:])
Regular Expression Exercises
Count Numbers
This is the count_numbers exercise in regexes.py.
Edit function count_numbers that returns a count of all numbers in a given string.
Hint
You can match a number by using a regular expression that matches one or more consecutive digits: \d+
>>> from regexes import count_numbers
>>> count_numbers(declaration)
{'4': 1, '1776': 1}
>>> count_numbers("Why was 6 afraid of 7? Because 7 8 9.")
{'7': 2, '9': 1, '6': 1, '8': 1}
Answers
import re
from collections import Counter
def count_numbers(string):
"""Return the count of all numbers in a given string."""
return dict(Counter(re.findall(r'\d+', string)))
Get File Extension
This is the get_extension exercise in regexes.py.
Edit the function get_extension that accepts a full file path and returns the file extension.
Example usage:
>>> from regexes import get_extension
>>> get_extension('archive.zip')
'zip'
>>> get_extension('image.jpeg')
'jpeg'
>>> get_extension('index.xhtml')
'xhtml'
>>> get_extension('archive.tar.gz')
'gz'
Answers
Works with examples given:
import re
def get_extension(filename):
"""Return the file extension for a full file path."""
return re.search(r'([^.]*)$', filename).group()
Works with no extension:
import re
def get_extension(filename):
"""Return the file extension for a full file path."""
match = re.search(r'\.([^.]*)$', filename)
return match.group(1) if match else ''
Works with only word-based extensions (try a.b/c):
import re
def get_extension(filename):
"""Return the file extension for a full file path."""
match = re.search(r'\.(?!.*\W)([^.]*)$', filename)
return match.group(1) if match else ''
Normalize JPEG Extension
This is the normalize_jpeg exercise in regexes.py.
Edit the function normalize_jpeg that accepts a JPEG filename and returns a new filename with jpg lowercased without an e.
Hint
Lookup how to pass flags to the re.sub function.
Example usage:
>>> from regexes import normalize_jpeg
>>> normalize_jpeg('avatar.jpeg')
'avatar.jpg'
>>> normalize_jpeg('Avatar.JPEG')
'Avatar.jpg'
>>> normalize_jpeg('AVATAR.Jpg')
'AVATAR.jpg'
Answers
import re
def normalize_jpeg(filename):
"""Return the filename with jpeg extensions normalized."""
return re.sub(r'\.jpe?g$', r'.jpg', filename, flags=re.IGNORECASE)
Count Punctuation
This is the count_punctuation exercise in regexes.py.
Edit the function count_punctuation that takes a string and returns a count of all punctuation characters in the string.
Punctuation characters are characters which are not word characters and are not whitespace characters
Hint
You can match punctuation characters with this regular expression: [^ \w]
>>> from regexes import count_punctuation
>>> count_punctuation("^_^ hello there! @_@")
{'^': 2, '@': 2, '!': 1}
>>> count_punctuation(declaration)
{',': 122, '.': 36, ':': 10, ';': 9, '-': 4, '—': 1, '’': 1}
Answers
import re
from collections import Counter
def count_punctuation(string):
"""Return count of all punctuation characters in given string."""
return dict(Counter(re.findall(r'[^ \w]', string)))
Normalize Whitespace
This is the normalize_whitespace exercise in regexes.py.
Edit the function normalize_whitespace that replaces all instances of one or more whitespace characters with a single space.
Example usage:
>>> from regexes import normalize_whitespace
>>> normalize_whitespace("hello there")
"hello there"
>>> normalize_whitespace("""Hold fast to dreams
... For if dreams die
... Life is a broken-winged bird
... That cannot fly.
...
... Hold fast to dreams
... For when dreams go
... Life is a barren field
... Frozen with snow.""")
'Hold fast to dreams For if dreams die Life is a broken-winged bird That cannot fly. Hold fast to dreams For when dreams go Life is a barren field Frozen with snow.'
Answers
import re
def normalize_whitespace(string):
"""Replace all runs of whitespace with a single space."""
return re.sub(r'\s+', r' ', string)
Hex Colors
This is the is_hex_color exercise in regexes.py.
Edit the function is_hex_color to match hexadecimal color codes. Hex color codes consist of an octothorpe symbol followed by either 3 or 6 hexadecimal digits (that’s 0 to 9 or a to f).
Example usage:
>>> from regexes import is_hex_color
>>> is_hex_color("#639")
True
>>> is_hex_color("#6349")
False
>>> is_hex_color("#63459")
False
>>> is_hex_color("#634569")
True
>>> is_hex_color("#663399")
True
>>> is_hex_color("#000000")
True
>>> is_hex_color("#00")
False
>>> is_hex_color("#FFffFF")
True
>>> is_hex_color("#decaff")
True
>>> is_hex_color("#decafz")
False
Answers
import re
def is_hex_color(string):
"""Return True iff the string represents an RGB hex color code."""
return bool(re.search(r'^#([\da-f]{3}){1,2}$', string, re.IGNORECASE))
import re
def is_hex_color(string):
"""Return True iff the string represents an RGB hex color code."""
return bool(re.search(r'^#[\da-f]{3}([\da-f]{3})?$', string, re.IGNORECASE))
import re
def is_hex_color(string):
"""Return True iff the string represents an RGB hex color code."""
return bool(re.search(r'^#([\da-f]{3}|[\da-f]{6})$', string, re.IGNORECASE))