Standard Library Answers

Random Exercises

Roll Die

This is the roll.py exercise in the modules directory. Create the file roll.py in the modules sub-directory of the exercises directory. To test it, run python test.py roll.py from your exercises directory.

Create a program roll.py that simulates rolling one or more dice. The number of sides on the dice should be provided as arguments. Each argument represents the number of sides of a die, so python roll.py 6 6 means rolling 2 6-sided dice. If no arguments are given, assume a single 6 sided die.

Examples:

$ python roll.py
5
$ python roll.py 6
4
$ python roll.py 20
10
$ python roll.py 6 6
7

Answers

import sys
import random


def roll_dice(*nums):
    if not nums:
        nums = [6]
    return sum(random.randint(1, n) for n in nums)

if __name__ == "__main__":
    print(roll_dice(*(int(n) for n in sys.argv[1:])))

Random Line

This is the randline.py exercise in the modules directory. Create the file randline.py in the modules sub-directory of the exercises directory. To test it, run python test.py randline.py from your exercises directory.

Create a program randline.py that accepts either an argument from the command-line or standard input text. The argument is treated as a file name and the standard input text is treated as lines of text. The program should return a random line from the file or from the standard input lines.

Example usage (with us-state-capitals.csv):

$ cat us-state-capitals.csv | python randline.py
Mississippi,Jackson
$ python randline.py us-state-capitals.csv
New Mexico,Santa Fe

Answers

With only libraries and syntax we have learned so far:

import random
import sys


def read_file_lines(file_name):
    with open(file_name) as in_file:
        return in_file.readlines()


def main(args):
    if not args:
        lines = sys.stdin.readlines()
    else:
        lines = read_file_lines(args[0])
    print(random.choice(lines).strip())


if __name__ == "__main__":
    main(sys.argv[1:])

With inline if statements and a tricky context manager:

import random
import sys


def read_file_lines(file_name=None):
    with open(file_name) if file_name else sys.stdin as in_file:
        return in_file.readlines()


def main(args):
    file_name = args[0] if args else None
    print(random.choice(read_file_lines(file_name)).strip())


if __name__ == "__main__":
    main(sys.argv[1:])

Using fileinput.input to read from files or sys.stdin:

import fileinput
import random
import sys


def read_file_lines(file_names):
    """Return lines from given files or standard input if no files given."""
    with fileinput.input(*file_names) as file_lines:
        return list(file_lines)


def main(args):
    print(random.choice(read_file_lines(args)).strip())


if __name__ == "__main__":
    main(sys.argv[1:])

Shuffle

This is the shuffle.py exercise in the modules directory. Create the file shuffle.py in the modules sub-directory of the exercises directory. To test it, run python test.py shuffle.py from your exercises directory.

Make a shuffle.py program that accepts an input file name and an output file name as input. The program should read the lines in the input file and write them to the output file in a random order.

$ cat dream.txt
What happens to a dream deferred?
Does it dry up
Like a raisin in the sun?
Or fester like a sore--
And then run?
Does it stink like rotten meat?
Or crust and sugar over--
like a syrupy sweet?
Maybe it just sags
like a heavy load.
Or does it explode?

$ python shuffle.py dream.txt nonsense.txt

$ cat nonsense.txt
Does it stink like rotten meat?
Or fester like a sore--
Or does it explode?
Does it dry up
Maybe it just sags
Or crust and sugar over--
Like a raisin in the sun?
What happens to a dream deferred?
like a heavy load.
And then run?
like a syrupy sweet?

Answer

import random
import sys


in_filename, out_filename = sys.argv[1:]


with open(in_filename, mode='rt') as in_file:
    lines = in_file.read().splitlines()
    random.shuffle(lines)

with open(out_filename, mode='wt') as out_file:
    for line in lines:
        out_file.write(line + "\n")

Capital Guessing

This is the guess_capitals.py exercise in the modules directory. Create the file guess_capitals.py in the modules sub-directory of the exercises directory. To test it, run python test.py guess_capitals.py from your exercises directory.

Write a guessing game program that takes a list of locations and their capitals and quizzes us on capitals.

You might want to test this program using us-state-capitals.csv.

Example usage:

$ python guess_capitals.py
Guess U.S. State Capitals

What is the capital of California? Sacramento
Correct!  You have correctly guessed 1 capitals in a row!

What is the capital of Texas? Dallas.
Sorry that's not right.

What is the capital of Texas? Austin
Correct!  You have correctly guessed 2 capitals in a row!

What is the capital of Nevada? Reno
Sorry that's not right.

What is the capital of Nevada? Las Vegas
Sorry that's not right.

What is the capital of Nevada? Paradise
Sorry that's not right.
The capital of Nevada is Carson City.

What is the capital of Ohio? Columbus
Correct!  You have correctly guessed 1 capitals in a row!

Answers

import csv
import random
import sys


def get_capitals(file_name):
    """Open capitals CSV file and return list of capital tuples."""
    with open(file_name) as csv_file:
        csv_reader = csv.reader(csv_file)
        next(csv_reader)  # Skip headers row
        return [(place, capital) for place, capital in csv_reader]


def main(file_name):
    capitals = get_capitals(file_name)
    place, capital = random.choice(capitals)
    while True:
        guess = input(f"What is the capital of {place}? ")
        if guess == capital:
            print(f"Correct! {capital} is the capital of {place}")
            break
        else:
            print("That's not correct.  Try again.")


if __name__ == "__main__":
    main(sys.argv[1])

Date Exercises

Random Date

This is the someday.py exercise in the modules directory. Create the file someday.py in the modules sub-directory of the exercises directory. To test it, run python test.py someday.py from your exercises directory.

Create a program someday.py that takes two dates as arguments and returns a random date between the two given dates.

Example usage:

$ python someday.py 1970-01-01 1979-12-31
1974-07-20
$ python someday.py 2099-12-01 2099-12-31
2099-12-19

Answers

Parse YYYY-MM-DD format and use timedelta and randint to select date

from datetime import datetime, timedelta
import random
import sys


DATE_FORMAT = "%Y-%m-%d"


def get_random_date_between(start_date, end_date):
    """Return random date between two given dates."""
    if end_date < start_date:
        start_date, end_date = end_date, start_date  # Order chronologically
    days_between = (end_date - start_date).days
    return start_date + timedelta(days=random.randint(0, days_between))


def main(start_date, end_date):
    start = datetime.strptime(start_date, DATE_FORMAT).date()
    end = datetime.strptime(end_date, DATE_FORMAT).date()
    print(get_random_date_between(start, end))


if __name__ == "__main__":
    main(*sys.argv[1:])

Fourth Thursday

This is the fourth_thursday exercise in dates.py.

The San Diego Python group meets on the fourth Thursday of every month. Make a function that takes a year and a month (January = 1, December = 12) and returns the fourth Thursday of that month.

>>> fourth_thursday(2015, 8)
datetime.date(2015, 8, 27)
>>> fourth_thursday(2015, 9)
datetime.date(2015, 9, 24)
>>> fourth_thursday(2015, 10)
datetime.date(2015, 10, 22)
>>> fourth_thursday(2016, 1)
datetime.date(2016, 1, 28)
>>> fourth_thursday(2016, 2)
datetime.date(2016, 2, 25)

Hint

Look up the weekday method on datetime objects.

Answers

Using datetime:

import datetime

def fourth_thursday(year, month):
    """Return a date of the fourth Thursday of the given month."""
    THURSDAY = 3
    first = datetime.date(year, month, 1)
    day_shift = datetime.timedelta((THURSDAY - first.weekday()) % 7)
    first_thursday = first + day_shift
    return first_thursday + datetime.timedelta(weeks=3)

Age

There are no tests for this exercise.

Make a function to calculate how old someone is given their birthdate as a datetime.date object. If today is February 4, 2016, your function should work like this:

>>> from datetime import date
>>> get_age(date(2015, 2, 4))
1
>>> get_age(date(2015, 2, 5))
0
>>> get_age(date(2015, 2, 3))
1
>>> get_age(date(1980, 2, 29))
35

Answers

import datetime

def get_age(birthdate):
    """Return age given a person's birthdate."""
    today = datetime.date.today()
    offset = (birthdate.month, birthdate.day) <= (today.month, today.day)
    return (today.year - birthdate.year) + offset - 1

Weekday Module

There are no tests for this exercise.

Make a Python script weekday.py that prints the day of the week for today. It should work like this:

$ python weekday.py
Thursday

Answers

import datetime


WEEKDAYS = ("Monday", "Tuesday", "Wednesday", "Thursday",
            "Friday", "Saturday", "Sunday")


def main():
    date = datetime.date.today()
    print(WEEKDAYS[date.weekday()])


if __name__ == "__main__":
    main()

Using weekday names in calendar.day_name:

import calendar
import datetime


def main():
    date = datetime.date.today()
    print(calendar.day_name[date.weekday()])


if __name__ == "__main__":
    main()

Collection Exercises

Count Words

This is the count_words exercise in dictionaries.py.

Modify the function count_words so that it accepts a string and returns a dictionary noting the number of times each word occurs in the string.

As a bonus exercise, ignore case and remove commas and other symbols between words.

Hint

Consider using a Counter or defaultdict from the collections module.

Note

To run tests for the Bonus exercise, open dictionaries_test.py, find the CountWordsTests class and comment out the @unittest.skip lines from the appropriate test methods.

Answers

With defaultdict:

from collections import defaultdict

def count_words(string):
    """Return the number of times each word occurs in the string."""
    count = defaultdict(int)
    for word in string.split():
        count[word] += 1
    return count

With Counter:

from collections import Counter

def count_words(string):
    """Return the number of times each word occurs in the string."""
    return Counter(string.split())

Bonus: Ignoring case and symbols:

from collections import Counter
import re

def count_words(string):
    """Return the number of times each word occurs in the string."""
    words = re.findall(r"[a-z']+", string.lower())
    return Counter(words)

Maximums

This is the MaxCounter exercise in classes.py.

Modify the class MaxCounter so that it acts just like the Counter class but MaxCounter objects should have a max_keys method that provides an iterable of all the keys that occur the most number of times.

For example:

>>> from classes import MaxCounter
>>> counts = MaxCounter('string of various words')
>>> counts
MaxCounter({'s': 3, 'r': 3, ' ': 3, 'o': 3, 'i': 2, 't': 1, 'n': 1, 'g': 1, 'f': 1, 'v': 1, 'a': 1, 'u': 1, 'w': 1, 'd': 1})
>>> counts.max_keys()
['s', 'r', ' ', 'o']

Answers

from collections import Counter


class MaxCounter(Counter):

    """Counter-like class that allows retrieving all maximums."""

    def max_keys(self):
        if not self:
            return set()
        (_, max_count), = self.most_common(1)
        maximums = set()
        for item, count in self.most_common():
            if count < max_count:
                break
            maximums.add(item)
        return maximums

Ice Cream DataClasses

Rewrite the Size, Flavor, and IceCream classes in classes.py using data classes.

You can test each of these classes by running tests for Size, Flavor, and IceCream.

Answers

from dataclasses import dataclass, field


@dataclass
class Flavor:

    """Flavor of ice cream."""

    name: str
    ingredients: list = field(default_factory=tuple)
    has_dairy: bool = True

    def __post_init__(self):
        """Copy given ingredients iterable."""
        self.ingredients = list(self.ingredients)


@dataclass
class Size:

    """Ice cream size."""

    quantity: int
    unit: str
    price: str

    def __str__(self):
        units = self.unit if self.quantity == 1 else f"{self.unit}s"
        return f"{self.quantity} {units}"


@dataclass
class IceCream:

    """Ice cream to be ordered in our ice cream shop."""

    flavor: Flavor
    size: Size

    def __str__(self):
        return f"{self.size} of {self.flavor.name}"

Most common

This is the get_most_common exercise in sets.py.

Create a function that accepts a list of iterables and returns a set of the most common items from all of the given iterables.

>>> get_most_common([{1, 2}, {2, 3}, {3, 4}])
{2, 3}

Hint

Consider using a Counter or defaultdict from the collections module.

Answers

With defaultdict:

from collections import defaultdict

def get_most_common(things):
    """Counter-like class that allows retrieving all maximums."""
    counts = defaultdict(int)
    for item_list in things:
        for x in item_list:
            counts[x] += 1
    most = max(counts.values())
    return {x for x, count in counts.items() if count == most}

With Counter:

from collections import Counter

def get_most_common(things):
    """Counter-like class that allows retrieving all maximums."""
    all_items = []
    for item_list in things:
        for x in item_list:
            all_items.append(x)
    counts = Counter(all_items)
    _, most = counts.most_common(1)[0]
    return {x for x, count in counts.items() if count == most}

With Counter and nested list comprehension:

from collections import Counter

def get_most_common(things):
    """Counter-like class that allows retrieving all maximums."""
    counts = Counter(x for item_list in things for x in item_list)
    _, most = counts.most_common(1)[0]
    return {x for x, count in counts.items() if count == most}

Flipping Dictionary of Lists

This is the flip_dict_of_lists exercise in collection.py.

Write a function that takes a dictionary of lists and returns a new dictionary containing the list items as keys and the original dictionary keys as list values.

Example:

>>> restaurants_by_people = {
...     'diane': {'Siam Nara', 'Punjabi Tandoor', 'Opera'},
...     'peter': {'Karl Strauss', 'Opera', 'Habaneros'},
...     'trey': {'Habaneros', 'Karl Strauss', 'Opera', 'Punjabi Tandoor'},
... }
>>> favorite_restaurants = flip_dict_of_lists(restaurants_by_people)
>>> favorite_restaurants
{'Siam Nara': ['diane'], 'Karl Strauss': ['trey', 'peter'], 'Opera': ['diane', 'trey', 'peter'], 'Punjabi Tandoor': ['diane', 'trey'], 'Habaneros': ['trey', 'peter']}

Hint

Consider using a defaultdict from the collections module.

Answers

from collections import defaultdict

def flip_dict_of_lists(original_dict):
    """Take dictionary of lists and return flipped dictionary of lists."""
    new_dict = defaultdict(list)
    for old_key, old_values in original_dict.items():
        for value in old_values:
            new_dict[value].append(old_key)
    return new_dict

Deal Cards

These are the get_cards, shuffle_cards and deal_cards exercises in collection.py.

Create three functions:

  • get_cards: returns a list of namedtuples representing cards. Each card should have suit and rank.

  • shuffle_cards: accepts a list of cards as its argument and shuffles the list of cards in-place

  • deal_cards: accepts a number as its argument, removes the given number of cards from the end of the list and returns them

Examples:

>>> from collection import get_cards, shuffle_cards, deal_cards
>>> deck = get_cards()
>>> deck[:14]
[Card(rank='A', suit='spades'), Card(rank='2', suit='spades'), Card(rank='3', suit='spades'), Card(rank='4', suit='spades'), Card(rank='5', suit='spades'), Card(rank='6', suit='spades'), Card(rank='7', suit='spades'), Card(rank='8', suit='spades'), Card(rank='9', suit='spades'), Card(rank='10', suit='spades'), Card(rank='J', suit='spades'), Card(rank='Q', suit='spades'), Card(rank='K', suit='spades'), Card(rank='A', suit='hearts')]
>>> len(deck)
52
>>> shuffle_cards(deck)
>>> deck[-5:]
[Card(rank='9', suit='diamonds'), Card(rank='6', suit='hearts'), Card(rank='7', suit='diamonds'), Card(rank='K', suit='spades'), Card(rank='7', suit='clubs')]
>>> hand = deal_cards(deck)
>>> hand
[Card(rank='9', suit='diamonds'), Card(rank='6', suit='hearts'), Card(rank='7', suit='diamonds'), Card(rank='K', suit='spades'), Card(rank='7', suit='clubs')]
>>> len(deck)
47
>>> deck[-5:]
[Card(rank='5', suit='spades'), Card(rank='Q', suit='clubs'), Card(rank='Q', suit='spades'), Card(rank='2', suit='diamonds'), Card(rank='6', suit='clubs')]

Answers

from typing import NamedTuple
import random

class Card(NamedTuple):
    """Class representing a playing card."""
    rank: str
    suit: str

def get_cards():
    """Create a list of NamedTuples representing a deck of playing cards."""
    ranks = ['A'] + [str(n) for n in range(2, 11)] + ['J', 'Q', 'K']
    suits = ['spades', 'hearts', 'diamonds', 'clubs']
    return [Card(rank, suit) for suit in suits for rank in ranks]

def shuffle_cards(deck):
    """Shuffles a list in-place"""
    random.shuffle(deck)

def deal_cards(deck, count=5):
    """Remove the given number of cards from the deck and returns them"""
    return [deck.pop() for i in range(count)]

Memory-efficient CSV

This is the parse_csv exercise in collection.py.

Using DictReader to read CSV files is convenient because CSV columns can be referenced by name (instead of positional order). However there are some downsides to using DictReader. CSV column ordering is lost because dictionaries are unordered. The space required to store each row is also unnecessarily large because dictionaries are not a very space-efficient data structure.

There is discussion of adding a NamedTupleReader to the csv module, but this hasn’t actually happened yet.

In the meantime, it’s not too difficult to use a csv.reader object to open a CSV file and then use a namedtuple to represent each row. See the collections.namedtuple for information about the original namedtuple.

Create a function parse_csv that accepts a file object which contains a CSV file (including a header row) and returns a list of namedtuples representing each row.

Example with us-state-capitals.csv:

>>> with open('us-state-capitals.csv') as csv_file:
...     csv_rows = parse_csv(csv_file)
...
>>> csv_rows[:3]
[Row(state='Alabama', capital='Montgomery'), Row(state='Alaska', capital='Juneau'), Row(state='Arizona', capital='Phoenix')]

Answers

from collections import namedtuple
import csv

def parse_csv(file):
    """Return namedtuple list representing data from given file object."""
    csv_reader = csv.reader(file)
    Row = namedtuple('Row', next(csv_reader))
    return [Row(*values) for values in csv_reader]

itertools Exercises

All Together

This is the all_together exercise in generators.py.

Write a function that takes any number of iterables and returns an iterator which will loop over each of them in order.

Example:

>>> from generators import all_together
>>> list(all_together([1, 2], (3, 4), "hello"))
[1, 2, 3, 4, 'h', 'e', 'l', 'l', 'o']
>>> nums = all_together([1, 2], (3, 4))
>>> list(all_together(nums, nums))
[1, 2, 3, 4]

Answers

def all_together(*iterables):
    """String together all items from the given iterables."""
    return (
        item
        for iterable in iterables
        for item in iterable
    )

lstrip

This is the lstrip exercise in iteration.py.

Edit the lstrip function so that it accepts an iterable and an object and returns an iterator that returns the items from the original iterable except any item at the beginning of the iterable which is equal to the given object should be skipped.

Look through the itertools library for something very useful for this.

Example:

>>> list(lstrip([0, 0, 1, 0, 2, 3], 0))
[1, 0, 2, 3]
>>> list(lstrip('  hello ', ' '))
['h', 'e', 'l', 'l', 'o', ' ']
>>> x = lstrip([0, 1, 2, 3], 0)
>>> list(x)
[1, 2, 3]
>>> list(x)
[]

Answers

from itertools import dropwhile


def lstrip(iterable, strip_value):
    """Return iterable with strip_value items removed from beginning."""
    def is_strip_value(value): return value == strip_value
    return dropwhile(is_strip_value, iterable)

Big Primes

This is the get_primes_over exercise in generators.py.

Write a function that returns an iterator that will result in a specified number of prime numbers greater than 999,999. The input to the function is the number of primes that will be generated by the iterator.

Try doing this without using while loops or for loops.

You can use this function to determine whether a number is prime:

def is_prime(candidate):
    """Return True if candidate number is prime."""
    for n in range(2, candidate):
        if candidate % n == 0:
            return False
    return True

Answers

from itertools import count, islice

def is_prime(candidate):
    """Return True if candidate number is prime."""
    for n in range(2, candidate):
        if candidate % n == 0:
            return False
    return True

def get_primes_over(limit):
    """Return primes over a given number."""
    primes = (
        n
        for n in count(999999)
        if is_prime(n)
    )
    return islice(primes, limit)

Total Length

This is the total_length exercise in iteration.py.

Make a function total length that should calculate the total length of all given iterables.

Example:

>>> total_length([1, 2, 3])
3
>>> total_length()
0
>>> total_length([1, 2, 3], [4, 5], iter([6, 7]))
7

Answers

With a chained list:

from itertools import chain


def total_length(*iterables):
    """Return the total number of items in all given iterables."""
    return len(list(chain.from_iterable(iterables)))

With sum of a generator containing 1 for each item:

from itertools import chain


def total_length(*iterables):
    """Return the total number of items in all given iterables."""
    return sum(1 for _ in chain.from_iterable(iterables))

Compact

This is the compact exercise in iteration.py.

Write a function compact that takes an iterable and lazily returns the elements of the iterable, with any adjacent duplicates removed.

Hint

Try using itertools.groupby with a generator expression to solve it.

It should work like this:

>>> from generators import compact
>>> list(compact([1, 1, 1]))
[1]
>>> list(compact([1, 1, 2, 2, 3, 2]))
[1, 2, 3, 2]
>>> list(compact([]))
[]
>>> c = compact(n**2 for n in [1, 2, 2])
>>> iter(c) is c
True
>>> list(c)
[1, 4]

Answers

from itertools import groupby


def compact(iterable):
    return (
        item
        for item, group in groupby(iterable)
    )

Stop On

This is the stop_on exercise in iteration.py.

Write a generator function stop_on that accepts an iterable and a value and yields from the given iterable repeatedly until the given value is reached.

Example:

>>> list(stop_on([1, 2, 3], 3))
[1, 2]
>>> next(stop_on([1, 2, 3], 1), 0)
0

Answers

from itertools import takewhile


def stop_on(elements, stop_item):
    """Yield from the iterable until the given value is reached."""
    def not_equal(item): return item != stop_item
    return takewhile(not_equal, elements)

Random Number

This is the random_number_generator exercise in iteration.py

Make an inexhaustible generator that always provides 4 as the next number.

Hint

Try using a tool from itertools to solve this problem.

Example:

>>> number_generator = random_number_generator()
>>> next(number_generator)
4
>>> next(number_generator)
4
>>> next(number_generator)
4
>>> iter(number_generator) is number_generator
True

Answers

from itertools import repeat

def random_number_generator():
    """Return a generator that yields 4 forever."""
    return repeat(4)

Running Mean

This is the running_mean exercise in iteration.py.

Create a running_mean function that takes an iterable and yields the current running mean.

Try doing this without a for loop (using only itertools function and generator expressions).

For example:

>>> numbers = [8, 4, 3, 1, 3, 5]
>>> list(running_mean(numbers))
[8.0, 6.0, 5.0, 4.0, 3.8, 4.0]

Answers

from itertools import accumulate
def running_mean(numbers):
    """Yield the running mean while looping over the iterable."""
    running_total = accumulate(numbers)
    return (
        total/length
        for length, total in
        enumerate(running_total, start=1)
    )

Argparse Exercises

Tip

For all of these programs, make sure your program prints useful help information when an incorrect number of arguments or invalid arguments are specified.

Note

These exercises do not work with the automated tests.

Add

This is the add.py exercise in the modules directory. You need to create the add.py file in the modules sub-directory of the exercises directory.

Make a program add.py that takes two numbers and prints out the sum of these numbers.

$ python add.py 3 5.2
8.2
$ python add.py -7 2
-5.0
$ python add.py -2 -1
-3.0

Answers

import argparse


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        'numbers',
        nargs=2,
        type=float,
        help="2 numbers to add together"
    )
    args = parser.parse_args()
    x, y = args.numbers
    print(x + y)


if __name__ == "__main__":
    main()

Average

This is the average.py exercise in the modules directory. You need to create the average.py file in the modules sub-directory of the exercises directory.

Make a program average.py that calculates the average of all given command-line arguments and prints a message “No numbers to average!” if there are no arguments given.

$ python average.py 2 3 4 5 6 7
Average is 4.5
$ python average.py 2 3 4
Average is 3.0

Answers

import argparse


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        'numbers',
        nargs='+',
        type=float,
        help="Numbers to average"
    )
    args = parser.parse_args()
    total = 0
    for num in args.numbers:
        total += num
    print(f"Average is {total/len(args.numbers)}")


if __name__ == "__main__":
    main()

Word Count

This is the wc.py exercise in the modules directory. You need to create the wc.py file in the modules sub-directory of the exercises directory.

Make a wc.py program that prints newline, word, and letter counts for given files. The command should take optional arguments and work in this way:

$ python wc.py --version
Word Count 1.0
$ python wc.py -h
wc - print newline, word, and letter counts for each file

Usage: wc.py [-hcwl] <file>...
wc.py (-h | --help)
wc.py --version

Options:
-c --chars    print the character count
-w --words    print the word count
-l --lines    print the newline count
-h --help     display this help and exit

The arguments should function like this:

$ python wc.py -c us-state-capitals.csv
us-state-capitals.csv:
chars 953
$ python wc.py -cl us-state-capitals.csv
us-state-capitals.csv:
lines 51
chars 953
$ python wc.py -l -c us-state-capitals.csv
us-state-capitals.csv:
lines 51
chars 953
$ python wc.py -w declaration-of-independence.txt
declaration-of-independence.txt:
words 1338
$ python wc.py declaration-of-independence.txt
declaration-of-independence.txt:
lines 67
words 1338
chars 8190
$ python wc.py -c declaration-of-independence.txt us-state-capitals.csv
declaration-of-independence.txt:
chars 8190
us-state-capitals.csv:
chars 953

Answers

"""wc - print newline, word, and letter counts for each file

Usage: wc.py [-hcwl] <file>...
wc.py (-h | --help)
wc.py --version

Options:
-c --chars    print the character count
-w --words    print the word count
-l --lines    print the newline count
-h --help     display this help and exit

"""
import argparse


def main():
    parser = argparse.ArgumentParser("wc")
    parser.add_argument('-c', '--chars', action='store_true',
                        help="print the character count")
    parser.add_argument('-w', '--words', action='store_true',
                        help="print the word count")
    parser.add_argument('-l', '--lines', action='store_true',
                        help="print the newline count")
    parser.add_argument("files", nargs="+")
    parser.add_argument(
        "--version",
        action="version",
        version="Word Count 1.0",
    )
    args = parser.parse_args()
    print_file_info(args.files, args.lines, args.words, args.chars)


def print_file_info(filenames, lines, words, chars):
    if not(chars or words or lines):
        chars = words = lines = True
    for filename in filenames:
        print(f"{filename}:")
        with open(filename, "rt") as handle:
            text = handle.read()
        if lines:
            print(f"lines: {len(text.splitlines())}")
        if words:
            print(f"words: {len(text.split())}")
        if chars:
            print(f"chars: {len(text)}")


if __name__ == '__main__':
    main()

Roll Die

This is the roll.py exercise in the modules directory. You need to create the roll.py file in the modules sub-directory of the exercises directory.

Create a program roll.py that simulates rolling one or more dice. The number of sides on the dice should be provided as arguments. If no arguments are given, assume a single 6 sided die.

Examples:

$ python roll.py
5
$ python roll.py 6
4
$ python roll.py 20
10
$ python roll.py 6 6
7

Answers

"""
Usage: roll.py [<dice>...]

Roll the given dice and display the sum of the rolls.

"""
import random
import argparse


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        'sides',
        nargs='*',
        type=int,
        help="Number of sides of each die"
    )
    args = parser.parse_args()
    print(roll_dice(*args.sides))


def roll_dice(*dice):
    if not dice:
        dice = [6]
    return sum(random.randint(1, n) for n in dice)


if __name__ == "__main__":
    main()

Convert CSV Delimiters

This is the convert_csv.py exercise in the modules directory. You need to create the convert_csv.py file in the modules sub-directory of the exercises directory.

Write a program that reads a CSV file using one delimiter format and outputs a new file using another delimiter format.

Example using the included file named people.csv that uses the | character as the delimiter, where we want it to be converted to a comma-separated file:

$ python convert_csv.py --in-delim="|" --out-delim="," people.csv people_comma.csv

Note

To run this script with escape characters (like tab) you may have to enter a tab key manually on your keyboard. You can usually do this with Ctrl-V Ctrl-TAB on Linux systems.

Answers

"""convert_csv files

Usage:
    convert_csv.py --in-delim=<delim> --out-delim=<delim> IN_FILE OUT_FILE

Convert the given CSV file from one delimiter format to another
"""
import argparse
import csv


def main():
    parser = argparse.ArgumentParser("convert_csv")
    parser.add_argument("--in-delim", help="Delimiter of input file")
    parser.add_argument("--out-delim", help="Delimiter of output file")
    parser.add_argument("in_file", help="Name of input CSV file")
    parser.add_argument("out_file", help="Name of output CSV file")
    args = parser.parse_args()
    convert(
        in_filename=args.in_file,
        out_filename=args.out_file,
        in_delimiter=args.in_delim,
        out_delimiter=args.out_delim,
    )


def convert(in_filename, out_filename,  in_delimiter, out_delimiter):
    with open(in_filename, mode='rt') as in_file:
        csv_reader = csv.reader(in_file, delimiter=in_delimiter)
        with open(out_filename, mode='wt', newline='') as out_file:
            csv_writer = csv.writer(out_file, delimiter=out_delimiter)
            csv_writer.writerows(csv_reader)


if __name__ == "__main__":
    main()

Difference Between

This is the difference.py exercise in the modules directory. You need to create the difference.py file in the modules sub-directory of the exercises directory.

Make a program difference.py that takes two numbers and prints the positive difference between these two numbers:

$ python difference.py 3 5
2.0
$ python difference.py 6 3.5
2.5
$ python difference.py -7 2
9.0

Answers

import argparse


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        'numbers',
        nargs=2,
        type=float,
        help="2 numbers to calculate difference"
    )
    args = parser.parse_args()
    x, y = args.numbers
    print(abs(x - y))


if __name__ == "__main__":
    main()

Pathlib Exercises

List Files

This is the ls.py exercise in the modules directory. You need to create the ls.py file in the modules sub-directory of the exercises directory.

Write a program ls.py that lists all files and directories in a given directory or in the current directory if no argument is given. Sort the lines by filename.

Example usage:

$ python3 ls.py
my_file.txt
speeches
whereami.py
$ python3 ls.py speeches/
moon_landing.txt

Answers

from pathlib import Path
import sys


def list_files(directory):
    for path in sorted(Path(directory).iterdir()):
        print(path.name)


if __name__ == "__main__":
    directory = Path(sys.argv[1]) if len(sys.argv) > 1 else Path('.')
    list_files(directory)

Remove Empty Directories

This is the remove_empty.py exercise in the modules directory. You need to create the remove_empty.py file in the modules sub-directory of the exercises directory.

Write a program remove_empty.py that removes all empty directories inside a given directory.

The empty directories should be removed recursively (if removing empty directories inside a parent directory makes that parent directory empty then it should be removed too).

Given this file hierarchy:

a
├── b
│   └── d
└── c
    └── e.txt

Our program should work like this:

$ python remove_empty.py
Deleting directory d
Deleting directory b
Deleting directory a

And our file hierarchy should now look like this:

a
└── c
    └── e.txt

Answers

from pathlib import Path
import sys


def remove_empty(directory):
    for path in directory.iterdir():
        if path.is_dir():
            remove_empty(path)
    if list(directory.iterdir()) == []:
        print(f"Deleting directory {directory.name}")
        directory.rmdir()


if __name__ == "__main__":
    remove_empty(Path(sys.argv[1]))

Find EditorConfig Files

This is the editorconfig.py exercise in the modules directory. You need to create the editorconfig.py file in the modules sub-directory of the exercises directory.

Write a module editorconfig.py that contains a function find_configs which accepts a filename and returns the contents of all .editorconfig files found in the file’s directory and all its parent directories.

Your editorconfig.find_configs function should return a list of tuples containing the file contents and the full file path of each .editorconfig file found like this.

For example, say we’re in the directory /home/trey/work/project/ and the following .editorconfig files exist:

/home/trey/work/project/.editorconfig:

[*.py]
indent_style = space
indent_size = 4

/home/trey/.editorconfig:

[*]
trim_trailing_whitespace = true

Calling our program should find and print those files like this:

>>> from editorconfig import find_configs
>>> for config, name in find_configs('/home/trey/work/project/my_file.py'):
...     print(f"Configuration for {name}")
...     print(config)
Configuration for /home/trey/work/project/.editorconfig
[*.py]
indent_style = space
indent_size = 4

Configuration for /home/trey/.editorconfig
[*]
trim_trailing_whitespace = true

Answers

from pathlib import Path


def find_configs(filename):
    config_files = [
        path / '.editorconfig'
        for path in Path(filename).resolve().parents
    ]
    return [
        (str(path), path.read_text())
        for path in config_files
        if path.is_file()
    ]

Tree

Note

This exercise does not have automated tests.

This is the tree.py exercise in the modules directory.

Write a program tree.py that prints out a file tree for the given directory (defaulting to the current directory of no argument is specified).

Also print out the total number of files and directories seen.

Example usage:

$ python3 tree.py
declaration-of-independence.txt
here/
    are/
      directories
speeches/
    moon_landing.txt
whereami.py

4 directories, 3 files

Bonus: Use ASCII art to represent the files visually

Example usage:

$ python3 tree.py
.
|-- declaration-of-independence.txt
|-- here
|   `-- are
|       `-- directories
|-- speeches
|   `-- moon_landing.txt
`-- whereami.py

4 directories, 3 files

Answers

Implementation using os.walk:

import os
import sys


def print_all_paths(tree, current_path, depth):
    """Print paths for file tree recursively.

    tree: mapping of full path to files and directories it contains
    current_path: current full path being prined
    depth: current path depth
    """
    for file_or_dir in tree[current_path]:
        printed_line = "    " * depth + file_or_dir
        full_path = os.path.join(current_path, file_or_dir)
        if full_path not in tree:
            print(printed_line)  # print just the file
        else:
            print(printed_line + "/")  # print directory
            print_all_paths(tree, full_path, depth + 1)


def print_file_tree(root):
    """Print file tree for given directory."""
    tree = {path: sorted(dirs + files) for path, dirs, files in os.walk(root)}
    print_all_paths(tree, root, 0)


if __name__ == "__main__":
    directory = sys.argv[1] if len(sys.argv) > 1 else os.getcwd()
    print_file_tree(directory)

Implementation using os.listdir:

import os
import re
import sys


def print_tree(top, prefix=""):
    """Print paths for file tree recursively."""
    print(prefix + os.path.basename(top))
    prefix = re.sub(r"[-`]", " ", prefix)
    try:
        children = os.listdir(top)
    except OSError:
        return  # This is a file, not a directory
    if not children:
        return  # Empty directory
    children = sorted(os.path.join(top, name) for name in children)
    for name in children[:-1]:
        print_tree(name, (prefix + "|-- "))
    print_tree(children[-1], (prefix + "`-- "))


if __name__ == "__main__":
    print_tree(sys.argv[1])