Python
Intro
Python 2 and 3 differences
print "fred" // OK Python 2
print("fred") // Not OK Python 2
Whitespace
Uses full colon and four spaces instead of brackets e.g.
for i in range(5):
x = i * 10
print(x)
Rules
- Prefer four spaces
- Never mix spaces and tabs
- Be consistent on consecutive lines
- Only deviate to improve readability
Help
help(object) gives help. e.g. for the module math
help(math)
Scalar Types, Operators, Control and Other
Types
- int (42)
- float (4.2)
- NoneType (None)
- bool ( True, False) 0 = False !=0 = True
Operators
- == value equality
- != value inequality
- < less-than
- > greater-than
- <= less-than or equal
- >= greater-than or equal
Control
if statementes
if True:
print("Its true")
h = 42
if h > 50:
print("Greater than 50")
elif h < 20:
print("Less than 20")
else:
print("Other")
while loops
while c != statement0:
print(c)
c -= 1 // c = c-1
print("Its true")
while True:
response = input()
if int(response) % 7 == 0:
break
for loops
cities = ["London", "Paris", "Berlin"]
for city in cities:
print(city)
Other
Conditional Expressions
No big surprise but
# Condition statement
if condition:
result = true_value
else:
result = false_value
# Condition expression (elvis result ? a:b
# result = true_value if condition else false_value
def sequence_class(immutable)
return tuple if immutable else list
Lambdas
Lambdas consist of the lambda keyword, argument separated by full colon and expression
lambda arg : expr
e.g.
is_odd = lambda x: x % 2 == 1
Looking a sorted the arguments are
sorted(iterable, key=None, reverse=False) --> new sorted list
The key argument must be a callable.
scientists = ['Maggie C', 'Albert E', 'Niels B']
# using a lambda, splits the names on space and this result is sorted
sorted(scientists, key=lambda name: name.split()[-1])
# Assigning shows
last_name = lamba name: name.split()[-1]
last_name
<function <lambda> at 0x103011c0
#e.g.
last_name("Fred Bloggs")
'Blogs'
# equivalent to
def first_name(name)
return name.split()[0]
Data types
Dates and Times
date
# 2014/1/6
datetime.date(2014,1,6)
datetime.date(year=2014,month=1,day=6)
# Now
datetime.date.today()
# Posix timestamp i.e. number of seconds from 1970 e.g. billionth second
datetime.date.fromtimestamp(1000000000) // datatime.data(2001,9,9)
time
datetime.time(3) // 3 hours
datetime.time(3,2) // 3 hours, 2 mins
datetime.time(3,2,1) // 3 hours, 2 mins, 1 sec
datetime.time(3,2,1,232) // 3 hours, 2 mins, 1 sec, 232 milliseconds
datetime
datetime.datetime(2003,5,12,14,33,22,245232) # 2003/05/12 14:33:22.245232
datetime.datetime.today() # Local now
datetime.datetime.now() # Local now
datetime.datetime.utcnow() # UTC now
# To combine
d = datetime.date.today()
t = datetime.time(8,15)
datatime.datetime.combine(d,t)
timedelta
These will hold the difference between two date times. e.g.
a = datetime.datetime(year=2014, month=5, day=8, hour=14, minutes=22)
b = datetime.datetime(year=2014, month=3, day=14, hour=12, minutes=9)
a-b
datetime.timedelta(55,7980)
timezones
Not sure the python people live in the real world. Default support seems poor
# Make one
cet = datetime.timezone(datetime.timedelta(hours=1), "CET")
# Make a datetime
departure = datetime.datetime(year=2014, month=1, day=7
hour=11, minute=30,
tzinfo=cet)
# Use default one
arrive = datetime.datetime(year=2014, month=1, day=7
hour=13, minute=5,
tzinfo=datatime.timezone.utc)
arrival - departure
datatime.timedelta(0,9300)
Decimal
This can be found in the decimal module and is precise to 28 places. Note the quotes in the examples as using no quotes means we are using floats - arggghhhh
Decimal('0.8') - Decimal('0.7')
# Result
Decimal('0.1')
# set this to stop usage of float constructors
decimal.getcontext().traps[decimal.FloatOperation] = True
# This will fail
Decimal0.8)
Fractions
Floating points come with problems when representing numbers such as 1/3 or other recurring values. The use of fractions provided by python may solve this.
# Two thirds
Fraction(2,3)
Complex Numbers
Python supports these by default
complex(3)
>>> (3+0j)
complex(3,2)
>>> (3+2j)
complex(3,10j)
>>> (3+10j)
Modulus in python
The standard approach to a%b = r is not how python implement this instead they use b*q + r = a. For example
In c++
#include <iostream>
int main()
{
auto a = -7;
auto b = 3;
auto c = (a) % b;
std::cout << "c = " << c << std::endl;
}
In python it uses b*q + r = a. See [[1]]
a = -7;
b = 3;
c = (a) % b;
print(c) // 2
-9 -8 -7 -6 -5 -4 -3 -2 -1 0
| | | | | | | | | |
--------------------------------------
q a
---------
r
The first number divisible by 3 is 9 if we travel negatively. The difference between this and the -7 is 2.
// Floor operator
Similar to the modulus, for integers this operates the same as the modulus and uses the next negative number going negative to calculate the answer
-9 -8 -7 -6 -5 -4 -3 -2 -1 0
| | | | | | | | | |
--------------------------------------
q a
---------
r
Therefore -7 // 3 = 3. The first number divisible by 3 is -9 if we travel negatively.
str
Double and single quotes are supported. Strings are immutable. Multiline
"""This is
a multiline
string"""
m = "This string\nspans multiple\nlines"
Raw Strings like c# @
path = r'C:\users\merlin\Documents'
Format string
m = "The age of {0} is {1}".format('Jim', 32)
print(m) // The age of Jim is 32
# Or without numbers
m = "The age of {} is {}".format('Jim', 32)
# f-strings are like c#
value = 3000
m = f"The value is {value}"
bytes
These work like strings, well ascii strings as and can be created like below
b'some bytpes'
print(b[0]) // 115
decoding to bytes
norsk = "some norsk characters"
data = norsk.encode('utf8')
norwegian = data.decode('utf8')
lists
General
List are a sequence of lists
m = [1,14,5]
// Can be different types
m = ['apple', 7, false]
// Add are mutable
b = []
b.append(1.666)
b.append(1.4444)
print(b) // [1.666, 1.4444]
// Constructor
print(list("characters")) // ['c','h','a','r','a','c','t','e','r','s']
Negative indexing
You can use negative indexing - errrr
s = [3,186,4431,74400, 1048443]
print(s[-1]) // 1048443
print(s[-2]) // 74400
Slicing
Subscript of lists can be achieved with the following
s = [3,186,4431,74400, 1048443]
print(s[1:3]) // 186, 4431
print(s[1:-1]) // 186, 4431, 74400
Dict
General
Dict are value pairs
m = {'1': 'Apple', '2': 'Orange'}
print(m['1']) // Apple
# Replaces
m['1'] = 'Banana']
print(m['1']) // Banana
# Update will add if it does not exist or replace
m.update(2:'Applie')
Set
Set are values like a dictionary with no key and must be unique
k = {91,109}
k.add(54)
# Error if not found
k.remove(91)
# No Error if not found
k.discard(91)
With sets we can compare. e.g.
blue_eyes = {'Olivia','Harry', 'Lily', 'Jack','Amelia'}
blond_hair = {'Harry', 'Jack','Amelia', 'Mia','Joshua'}
# Combined
print(blue_eyes.union(blond_hair)) // {'harry','Jack','Amelia','Joshua','Mia','Olivia','Lily'}
# In both
print(blue_eyes.intersection(blond_hair)) // {'harry','Jack','Amelia'}
# Not in this
print(blond_hair.difference(blue_eyes)) // {'Mia','Joshua'}
# Not in other
print(blond_hair.symmetric_difference(blue_eyes)) // {'Mia','Joshua','Olivia','Lily'}
Tuples
Tuples look like lists but have round brackets.
t = ('Apple', 3.5, False)
# to make a single you need to use the trailing comma or it thinks it is a single type e.g.
t = ('Apple',)
# to index one with pairs use second index e.g
t = ((220,284),(220,285),(220,284),(220,281))
print(t[0][1])
Unpacking like javascript works and swapping
def minmax(items):
return min(items), max(items)
lower, upper = minmax([83, 33, 84,32, 85, 31, 86])
print(lower) // 31
print(upper) // 86
a = 'Apple'
b = 'Pear'
a, b = b, a
print(a) // Pear
print(b) // Apple
Ranges
Range supports arguments stop, start, stop or start, stop, step. e.g.
# 0-5
range(5)
# 10-20
range(10,20)
# 10-20 step 2
range(10,20,2)
Map function
Intro
This is similar to the javascript function. It creates an map object which can be iterated on a runtime. i.e. it does not produce a list only an object which next can be used on,
f = map(ord, "the quick brown fox")
a = next(f)
b = next(f)
c = next(f)
print(a) # 84
print(b) # 104
print(c) # 101
Multi Sequences
If the function needs more args you pass more args. The map ends when any of the sequences ends
sizes = ['small','medium','large']
colors = ['lavendar','teal','burnt orange']
animals = ['koala','platypus','salamander']
def combine(size,color,animal):
return '{},{},{}'.format(size,color,animal)
list(map)combine,sizes,colors, animals))
>> ['small lavender koala','medium teal platypus','large burnt orange salamander']
Filter function
Intro
This accepts a function and a single sequence and like map returns an object not a result. Only the elements which return True are returned.
myObject = filter(is_odd, [1,2,3,4,5,6,7])
None
You can pass None as the function and only the true objects are returned
myObject = filter(None, [0,1, False,True, [], [1,2,3],'','hello'])
>> [1, True, [1,2,3],'hello'])
Reduce
Repeatedly apply a function to the elements of a sequence reducing them to a single value
reduce(operator.add, [1,2,3,4,5])
>>15
# With start value
reduce(operator.add, [1,2,3,4,5],100)
>>115
Comprehensions
List Comprehension Syntax
Generally this is
[expr(item) for item in iterable]
words = "Why sometimes I have believed"
print([len(word) for word in words]) // [3,9, 1, 4, 8]
These can be more complex. e.g.
[(x,y) for x in range(5) for y in range(3)]
[(0,0),(0,1),(0,2),(1,0),(1,1),(1,2),(2,0),(2,1),(2,2),(3,0),(3,1),(3,2),(4,0),(4,1),(4,2)]
# Which is the same as
point = []
for x in range(5)
for y in range(3)
points.append((x,y))
points
Dict Comprehensions
Like lists above
{expr(key:) expr(value) for item in iterable}
country_to_capital = { 'UK': 'London',
'Brazil': 'Brasilia',
'Sweden': 'Stockholm' }
capital_to_country = { capital: country for country, capital in country_to_capital.items()}
print(capital_to_country) // {'Brasilia': Brazil, 'London': 'UK', 'Stockholm': 'Sweden'}
Iteration
Iterators
Here is how to iterate
s = [1,2,3,4]
myIterator = iter(s)
item1 = next(myIterator)
print(item1) // 1
item2 = next(myIterator)
print(item2) // 2
Writing Own Iterator
Just implement __iter__ and __next__
class ExmapleIterator
def __init__(self,data):
self.index = 0
self.data = data
def __iter__(self):
return self
def __next__(self):
if self.index >= len(self.data):
raise StopIteration()
rslt = self.data[self.index]
self.index += 1
return rslt
Using second argument of iter
The second argument of iter allows you to test the result and exit if True. e.g.
# You should
# see this
# text.
# END
# But not
# this text.
with open('the_above_text.txt', 'rt') as f:
for line in iter(lambda: f.readline().strip(), 'END');
>> You should
>> see this
>> text.
Generators
Generator functions
This is just like javascript redux stuff
def gen123():
yield 1
yield 5
yield 3
myIterator = gen123()
print(next(myIterator)) // 1
print(next(myIterator)) // 5
print(next(myIterator)) // 3
print(next(myIterator)) // Exception
# Or
for v in gen123():
print(v)
...
1
5
3
Generator Expressions
Syntax can be defined as
(expr(item) for item : iterable)
million_squares = (x*x for x in range(1,1000001))
# Generate and output last 10
list(million_squares)[-10:]
# Again will yield nothing
list(million_squares)
Iteration tools
islice
from itertools import count, islice
thousand_primes = islice( (x for x in count() if is_prime(x), 1000)
# thousand_primes is a special islice object which is iterable
# converting to a list
list(thousand_primes)[-10:]
[7841,7853, ..... 7919]
# so to sum first thousand primes
sum(islice( (x for x in count() if is_prime(x), 1000))
3682913
zip
Combine groups together e.g.
sunday = [10,20,30]
monday = [101,201,301]
for item in zip(sunday, monday)
print(item)
...
(10,101)
(20,201)
(30,301)
Exceptions
General
def convert(s):
try:
number = ''
for token in s:
number += DIGIT_MAP[token]
x = int(number)
# Can be on one line
# except (KeyError, TypeError):
except TypeError:
x = -2
raise # rethrow
except KeyError:
x = -1
raise # rethrow
return x
Functions
General Functions
These are created as below
def foo(arg1, arg2):
return arg1 * arg2
Default Arguments
def foo(arg1, arg2=9):
return arg1 * arg2
Be aware that the def assignment is only run once. Therefore These are created as below
def add_spam(menu=[]):
menu.append('spam')
add_spam() // ['spam']
add_spam() // ['spam','spam']
Advice is to make default arguments not mutable. i.e. not strings and not ints
def add_spam(menu=None):
if(menu==None)
menu = []
menu.append('spam')
return menu
add_spam() // ['spam']
add_spam() // ['spam']
Extended Formal Arguments (params)
Intro
Remember we have positional and keyword arguments in python
Positional arguments
def with an argument prefixed with an asterix means the arguments being passed are a tuple. e.g.
def test(*arg):
print(args)
print(type(args))
test(1,2,3)
1,2,3
<class tuple)
Keyword arguments
def with an argument prefixed with two asterix means the arguments being passed are a dict. e.g.
def test(name, **kwargs):
print(name)
print(kwargs)
print(type(kwargs))
test('img', src="monet.jpg", alt="Sunrise by Claude", border=1)
img
{'src':'monet.jpg', 'border':'1', 'alt':'Sunrise by Claude'}
<class dict)
Extended Call Syntax
Equally the calling of functions can use keyword two asterix. Doing so means the positional parameters are satisfied and the remaining parameters are used to make keyword arguments. e.g.
def color(red, green, blue, **kwargs):
print("r =", red)
print("g =", green)
print("b =", blue)
print(kwargs)
k = {'red': 21,'green': 22,'blue': 23,'alpha': 24, 'beta': 25}
color(**k)
r = 21
g = 22
b = 23
{'alpha' :24, 'beta': 25}
Returning Functions
Intro
In python you can return a function and execute it.
def enclosing():
def local_function():
print('Hi')
return local_function
lf = enclosing()
lf() # // Hi
Factories
We can combine the values are creation of the function with the arguments of the execution of the function. Look at variable exp which is created on execution of raise_to e.g.
def raise_to(exp):
def raise_to_exp(x):
return pow(x,exp)
return raise_to_exp
myfoo = raise_to(2)
myfoo(10) # // 100
myfoo(5) # // 25
Decorators
Intro
Like c# the functions can be decorated. e.g.
def escape_unicode(f):
@functiools.wraps(f)
def wrap(*args, **kwargs):
x = f(*args, **kwargs)
return ascii(x)
return wrap
@escape_unicode
def northern_city()
return ;'Troms0'
northern_city() // "'Troms\\xf8'"
The functools.wrap is necessary to help the support tools such as help.
With parameters
Like typescript you can pass arguments to your decorator by wrapping a decorator in a function and returning the decorator. e.g.
def validator(f):
# Start of Decorator
def wrap(*args):
@functiools.wraps(f)
if(args[index] < 0:
raise ValueError("Argument {} must be non-negative.'.format(index))
return f(*args)
return wrap
# End of Decorator
return validator
@check_non_negative(1)
def create_list(value, size):
return [value] * size
The functools.wrap is necessary to help the support tools such as help.
Class Decorator
Instances of Classes can be used as Decorators provide they implement the __call__ method
Multiple Decorator
Decorators can be multiple. They are executed in reverse order. i.e. decorator1, decorator2
@decorator1
@decorator2
def northern_city()
return ;'Troms0'
Modularity
Importing defs
Best to be selective
from words import (fetch_words, print_words)
// could be BAD BAD!!
from words import *
Passing arguments
import sys
if __name__ == '__main__':
main(sys.argv[1])
Comments
def fetch_words(url):
"""Fetch a list of words from a URL.
Args:
url: The URL of UTF-8 text document.
Return:
A list of strings containing the words from
the document.
"""
story = urlopen(url)
story_words = []
for line in story:
line_words = line.decode('utf8').split()
for word in line_words:
story_words.append(word)
story.close()
return story_words
Scope of Objects
Types of Scope
- Local - Inside current function
- Enclosing - Inside enclosing function
- Global - At the top level of the module
- Built-in - In the special builtins module
Overriding Scope
global
Not using global creates a new count and it shadows the global count.
count = 0
def show_count():
print(count)
def set_count(c)
global count = c
set_count(5)
show_count()
nonlocal
Where there are functions within functions the nonlocal keyword may be used. e.g.
count = 0
def enclosing():
count = 5
def local():
nonlocal count
count = 25
Objects and Types
Named references to objects
Assigning variables is the same as references. Use id() to prove this.
s = [1,2,3]
r = s
s[0] = 500
print(r)
[500,2,3]
p = [4,5,6]
q = [4,5,6]
print(p == q) // True
print(p is q) // False
Passing Arguments are like references
Passing arguments is like passing references
m = [9,15,24]
def modify(k):
k.append(39)
print("k = ", k)
modify(m)
k = [9,15,24, 39]
print(m)
[9,15,24, 39]
Passing Arguments are like references II
Or are they. g is reassigned not mutated
f = [14, 23, 37]
def replace(g):
g = [17,28, 45]
print("g = ", g)
replace(f)
g = [17,28, 45]
print(f)
[14,23,37]
Classes
General
class Fight:
def __init__(self, registration, model, num_rows)
self._registration = registration
self._model = model
self._num_rows = num_rows
def registration(self):
return self._registration
def model(self):
return self._model
def num_rows(self):
return self._num_rows
Access
There is no public, protected or private in Python
Inheritance
Intro
This is achieved using brackets on the name
class MyBaseClass:
def registration(self):
return self._registration
def model(self):
return self._model
def num_rows(self):
return self._num_rows
class Fight(MyBaseClass):
def __init__(self, registration, model, num_rows)
self._registration = registration
self._model = model
self._num_rows = num_rows
Multiple Inheritance
Python supports this. For initializers, only the first base class is automatically called. Where there are methods are defined the same the MRO or Method Resolution Order is used. This can be seen with classname.__mro__. This can also be obtained by calling classname.mro(). In general the class is search in declaration order.
class Fight(MyBaseClass1,MyBaseClass2,MyBaseClass3):
def __init__(self, registration, model, num_rows)
self._registration = registration
self._model = model
self._num_rows = num_rows
</syntaxhighlight>
Base Class Init
This is not called by default. To call the base class call super. e.g.
class RefridgeratedShippingContainer(ShippingContainer):
MAX_CELSIUS = 4.0
def __init__(self, owner, contents, celsius):
super().__init__(owner, contents)
Factories for Derived Classes
Using extended call arguments we can work around creating derived classes using base class. e.g.
class BaseClass:
def create_default(cls, attr1):
return cls(attr1, *args, **kwargs)
def __init__(self, attr1):
self._attr1 = attr1
class DervivedClass(BaseClass):
def __init__(self, attr1, attr2):
self._attr1 = attr1
self._attr2 = attr2
f = DervivedClass.create_default('A1','A2')
Static methods
Note if you are calling static methods on classes you should use self and not the class name as this will provide polymorphic behavior unless you do not want this :)
String and Representations
Bit of python up themselves here. Basically repr is for developers and explicit where str is for clients.
class Point2D
def __init__(self,x,y):
self.x = x
self.y = y
def __str__(self):
return '({}, {})'.format(self.x, self.y)
def __repr__(self):
return 'Point2d(x={}, y={})'.format(self.x, self.y)
Properties
Getters and Settters
Not great but this appears to be like this
class MyClass:
# Getter
@property
def myattribute(self)
return self._myattribute
# Setter
@myattribute.setter
def myattribute(self,value)
self._myattribute = value
Derived Class Getters and Settters
In derived class the the getter can be overridden by just redefining. Setter requires you to reference the class which contains the property. e.g.
class MyClass:
# Getter
@property
def myattribute(self)
return self._myattribute
# Setter
@myattribute.setter
def myattribute(self,value)
self._myattribute = value
class Derived(MyClass):
# Setter
@MyClass.myattribute.setter
def myattribute(self,value)
if(value > 10):
raise ValueError("Value out of range")
self._myattribute = value
Horrible access to base class setter
You can access this be calling the baseclassname.attribute.fset(self,value). Which is horrible like this.
class MyClass:
# Getter
@property
def myattribute(self)
return self._myattribute
# Setter
@myattribute.setter
def myattribute(self,value)
self._myattribute = value
class Derived(MyClass):
# Setter
@MyClass.myattribute.setter
def myattribute(self,value)
if(value > 10):
raise ValueError("Value out of range")
MyClass.myattribute.fset(self,value)
__call__
No idea why this is good but essentially it allows to call an instance of an object with no method or rather the method name __call__
class Test
def __init__(self)
self._cache = {}
def __call__(self, arg1)
if arg1 not in self._cache:
self._cache[arg1] = socket.gethostbyname(arg1)
return self_cache[host]
f = Test()
f('bibble.co.nz')
Static Attributes
You qualify the attribute with the class name
class Test:
a_static = 112
def __init__(self, registration, model, num_rows)
self._registration = registration
self._model = model
self._num_rows = num_rows
Test.a_static = Test.a_static + 1
Static Method
Intro
These seem very similar. The tutorial said the rule is simple if you need to refer to the class object within the method, e.g. a class attribute, use class method.
@staticmethod
No access needed to either class or instance objects.
class Test:
a_static = 1337
@staticmethod
def _get_next_serial():
result = Test.a_static
Test.a_static = += 1
return result
def __init__(self, registration, model, num_rows)
self._registration = registration
self._model = model
self._num_rows = num_rows
Test.a_static = Test._get_next_serial()
@classmethod
Requires access to the class object to call other class methods or the constructor
class Test:
a_static = 1337
@classmethod
def _get_next_serial(cls):
result = cls.a_static
cls.a_static = += 1
return result
def __init__(self, registration, model, num_rows)
self._registration = registration
self._model = model
self._num_rows = num_rows
self.a_static = Test._get_next_serial()
A typical use may be a factory. e.g.
class Test:
a_static = 1337
@classmethod
def create_empty_test(cls):
return cls("","", 0)
@classmethod
def create_default_test(cls):
return cls("XXX","YYY", 1)
def __init__(self, registration, model, num_rows)
self._registration = registration
self._model = model
self._num_rows = num_rows
self.a_static = Test._get_next_serial()
File IO
The with is used like using in c#
Reading
with open('dog_breeds.txt', 'r') as reader:
# Read and print the entire file line by line
line = reader.readline()
while line != '': # The EOF char is an empty string
print(line, end='')
line = reader.readline()
Packages
Packages
Python finds packages by looking at sys.path. You can see this by doing
import sys
sys.path
# For entry 0
sys.path[0]
# To add you can
sys.path.append('/mypath');
Another approach is to add your path to PYTHONPATH
export PYTHONPATH=$PYTHONPATH:/mypath
Make a Package
mkdir -p /mypath/reader
touch /mypath/reader/__init__.py
For a simple reader class the contents of __init__.py may be (absolute)
from reader.reader import Reader
For a simple reader class the contents of __init__.py may be (relative)
from .reader import Reader
Controlling whats imported
You can do this by specifying the __all__ content. Looks like a def file in windows dlls. e.g.
from reader.compressed.bzipped import opener as bz2_opener
from reader.compressed.gzipped import opener as gzip_opener
__all__ = ['bz2_opener', 'gzip_opener']
Namespace packages
These are packages split across to directories and the root directories do not contain a __init__.py. Importing namespace packages
- Python scans all entries in sys.path
- if a matching directory with __init__.py is found, a normal package is loaded
- if foo.py is found then it is loaded
- Otherwise, all matching directories in sys.path are considered part of a namespace package
path1
|
--split_farm
|
-- bovine
|
-- __init__.py
-- common.py
-- cow.py
-- ox.py
path2
|
--split_farm
|
-- bird
|
-- __init__.py
-- chicken.py
-- turkey.py
Executable Directory
You can make a executable by providing a __main__.py in the directory.
project
|
-- __main__.py
-- project
|
-- __init__.py
-- stuff.py
-- setup.py
You can then run the code with
python3 reader
Zipping up the directory and it can be distributed as python treats zips a directories. e.g.
python3 reader.zip