Good habits when writing small console modules and scripts in Python 3
As you can see on my blog, I write in Python (3) a lot. I struggled with several things when I had been writing the scripts and small modules. Here is what I basically needed:
- Handling the output in logging like manner. That means to have ability to set up logging level (different for debug, info, or silent mode). This should be scalable so I can use this logging settings in several modules with minimal code needed.
- Parsing command line arguments
- logging should be set by reading the logging config file
- have a possibility to turn on/off debug mode easily
- have a possibility to terminate process by CTRL+C nicely
- Nice layout of code and readability (for maintenance). In the best, everything wrapped in functions and objects. Not something like this:
# beginning of my file
print("Hello. This is on the beginning of the file")
from argparse import ArgumentParser
# some stuffs happens here
def some_func():
# doing something
pass
class SomeObject():
# object from hell
pass
# ...
if __name__ == "__main__":
ob = SomeObject()
res = some_func(ob)
code above just seems painful to handle. My final layout of these files is as follows:
- one main file, sometimes called
main.py
orprogram_name.py
. This is what is being ran when starting the program. - importing
logging
module at the beginning of the main file (this is only code which is not wrapped). It's necessary because I want to have logger available from the beginning. Then simply getting the main logger in every app. - in
if __name__ == "__main__"
I callmain()
method. - argument parsing happens in
argument_parsing()
method inmain()
and returnsargs
object.args
object is not going anywhere else then inmain()
method. If I need things fromargs
object, I just pass them as arguments to the methods or classes constructors. This helps to leave the code clean and easy to debug. - if a program needs more than one class, I usually split it to several modules.
- methods is usually better to wrap under some class
- in arguments, I almost always add
--debug
flag which turns logging toDEBUG
level.
So here is an example:
"""
Docstring describing the whole progam
"""
import logging
from logging.config import fileConfig
fileConfig('logging_config.ini')
log = logging.getLogger()
class MainProgramObject():
def __init__(self, arg1, arg2):
log.debug("Initilizing object.")
self.arg1 = arg1
self.arg2 = arg2
def some_logic(self, ):
log.info("I'm doing nasty stuffs from module another_module.py")
from another_module import DidIt
dit = DidIt(self.arg1)
return dit.get_result()
def parse_arguments():
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
parser = ArgumentParser(description="Brief description of program", formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument( "-a", "--arg1", type=int, default = "HiHi", help = "First argument!")
parser.add_argument( "-b", "--barg2", type=str, default = "HeHe", help = "Second argument!")
parser.add_argument( "-d", "--debug", action="store_true", help = "Turn on debug mode." )
args = parser.parse_args()
return args
def main():
args = parse_arguments()
if args.debug:
log.setLevel(logging.DEBUG)
log.debug("Log level set to {}".format(log.level))
try:
mainObj = MainProgramObject(args.arg1, args.barg2)
result = mainObj.some_logic()
print result
except KeyboardInterrupt as ex:
log.warning("Termined by user.")
if __name__ == "__main__":
main()
an example of another_module.py
might look like this:
"""
Again, describe what is going here
"""
import logging
log = logging.getLogger(__name__)
class DidIt():
def __init__(self, argums):
log.debug("Working on this arg {}".format(self.argums))
self.argums = argums
def get_result(self):
# example of doing some stuffs
return type(self.argums)
This is the best workflow I've found as far. But there of course might be better ways ;) .
And the logging_config.ini
looks like this:
[loggers]
keys=root
[handlers]
keys=stream_handler
[formatters]
keys=formatter
[logger_root]
level=INFO
handlers=stream_handler
[handler_stream_handler]
class=StreamHandler
level=DEBUG
formatter=formatter
args=(sys.stderr,)
[formatter_formatter]
format=%(asctime)s %(name)-12s %(levelname)-8s %(message)s