I bet I’ve written hundreds of Python scripts during my career that take a date parameter on the command line. I often need short utility scripts that can run on a subset of data limited by dates. My preferred way to parse command line options in Python is to use the core argparse
module. But argparse doesn’t have built in support for date handling. The first time I used it, I did all the date parsing after I had done the argument parsing. In this article, I’ll show you two simple ways to add date argument parsing directly to your command line parsing.
First, let’s do a quick review of argparse
and how it works. Then we’ll add a simple date parser, followed by a slightly more complex one.
argparse
basics
The argparse
module has a good tutorial that I’d recommend as a starting point if you’ve never written a command line script in Python before, or haven’t used argparse
. I’m going to assume that you at least know how to create an ArgumentParser
and have it parse some basic arguments of different types. Let’s look at an example. Note that since this example code is running inside a Jupyter notebook, I’ll always pass in my own arguments here, but in your command line script, you’ll just call parser.parse_args()
to parse the arguments from the command line.
import argparse parser = argparse.ArgumentParser() # since this example is running in Jupyter, I'll always pass in the arguments try: parser.parse_args(["-h"]) except SystemExit: # calling help will call SystemExit, we can catch this instead pass
usage: ipykernel_launcher.py [-h] optional arguments: -h, --help show this help message and exit
OK, let’s add a few arguments of different types.
parser = argparse.ArgumentParser() parser.add_argument('-n', '--number', type=int, help='Number of times to run') parser.add_argument('-x', '--extension', type=str, help='File extension to search for') parser.add_argument('-d', '--debug', action='store_true', help='Turn on debug logging') parser.parse_args(["-n", "10", "--extension", ".xls", "-d"])
Namespace(debug=True, extension='.xls', number=10)
Now let’s try it with some sort of date argument. Let’s do the naive thing and just see if we could set the type to a date. Will that just work?
import datetime parser = argparse.ArgumentParser() parser.add_argument('-s', '--start', type=datetime.date, help='Set a start date') try: parser.parse_args(["-s", "2022-01-01"]) except: pass
usage: ipykernel_launcher.py [-h] [-s START] ipykernel_launcher.py: error: argument -s/--start: invalid date value: '2022-01-01'
Alas, it’s not quite that easy. I didn’t show you the full output (I put this in a try/except), but the reason it doesn’t work is because datetime.date
doesn’t take a single string argument in its contructor. The type
argument in parser.add_argument
can be any callable that takes a single string, and in this scenario argparse
is just passing that string into the date
constructor, which doesn’t work. It is expecting three int arguments instead.
So let’s do some basic date parsing like this:
parser = argparse.ArgumentParser() parser.add_argument('-s', '--start', type=lambda d: datetime.datetime.strptime(d, '%Y-%m-%d').date(), help='Set a start date') parser.parse_args(["-s", "2022-01-01"])
Namespace(start=datetime.date(2022, 1, 1))
I’ll break that down a bit. The lambda
takes one parameter. It will be passed the command line string token (which is 2022-01-01
in this case) and pass it into datetime.datetime.strptime
with the right format, and then call date()
on it to return just the date portion.
More complex date parsing
But what if you want to accept dates in multiple formats?
You could create a more complex lambda or a separate function to do this. But it turns out that someone has of course already created this, and it’s called dateutil
. You can install it if it’s not already in your environment with pip install python-dateutil
. The parse
method will do a pretty good job of getting a valid date out of a string.
To wire this parsing up to our argument parsing, the argparse
API can be extended via custom Action
classes. The action class only needs to implement the __call__
method to take the correct arguments and deal with the values passed to it properly. The function signature includes the argparse.Namespace
object and it’s recommended that you store the results of your processing in the namespace by the correct name, using setattr
. We connect it to the argument parser by specifying it as the action for our argument.
from dateutil.parser import parse, ParserError class DateParser(argparse.Action): def __call__(self, parser, namespace, values, option_strings=None): setattr(namespace, self.dest, parse(values).date()) parser = argparse.ArgumentParser() parser.add_argument('-s', '--start', action=DateParser, help='Set a start date') parser.parse_args(["-s", "1/1/2022"])
Namespace(start=datetime.date(2022, 1, 1))
This works with a pretty wide variety of date formats. It will also raise a ValueError
(actually a subclass of it) if there’s a parsing error.
assert parser.parse_args(["-s", "1/1/2022"]).start == datetime.date(2022,1,1) assert parser.parse_args(["-s", "2022-1-1"]).start == datetime.date(2022,1,1) assert parser.parse_args(["-s", "Jan1,2022"]).start == datetime.date(2022,1,1) assert parser.parse_args(["-s", "1-Jan-2022"]).start == datetime.date(2022,1,1) assert parser.parse_args(["-s", "1-Jan-22"]).start == datetime.date(2022,1,1) assert parser.parse_args(["-s", "January, 1 2022"]).start == datetime.date(2022,1,1) assert parser.parse_args(["-s", "January 1st, 2022"]).start == datetime.date(2022,1,1) try: parser.parse_args(["-s", "tomorrow"]) except ValueError as ex: print(ex)
Unknown string format: tomorrow
There you go, you can now easily add flexible date parsing of command line options to your Python scripts. I have to do this so often that I usually copy this code out of an existing script. I hope you find this article useful.