Tags

, ,

Simple Tricks #10 was about Python classes with a focus on the __new__ and __init__ built-in methods plus how to use them when extending Python’s built-in list, tuple, and dict classes.

In this edition of Simple Tricks, we’ll look at a number of possibly actually useful subclasses of Python’s dict class. Specifically, a “ticket” class, a “list of files” class, and an INI file class.

But we’ll start with a “do nothing” subclass of dict that instruments the built-in functions most central to dict:

001| class test_bag (dict):
002| 
003|     def __new__ (cls, **kwargs):
004|         “””Create a new test_bag instance.”””
005|         print(f’new({kwargs.keys()})’)
006| 
007|         obj = super().__new__(cls, **kwargs)
008|         print(f’self={obj}’)
009|         return obj
010| 
011|     def __init__ (self, **kwargs):
012|         “””Initialize a new test_bag instance.”””
013|         print(f’init({kwargs.keys()})’)
014|         super().__init__()
015| 
016|         # Populate self “by hand”…
017|         for kx,k in enumerate(kwargs):
018|             a = kwargs[k]
019|             print(f’arg[{kx}] {k}=”{a}”‘)
020|             self[k] = a
021| 
022|     def __missing__ (self, keyname):
023|         “””No entry in collection for keyname.”””
024|         print(f’missing({keyname})’)
025| 
026|         return None
027| 
028|     def __getitem__ (self, keyname):
029|         “””Get Item. val = obj[key]”””
030|         print(f’getitem({keyname})’)
031| 
032|         return super().__getitem__(keyname)
033| 
034|     def __setitem__ (self, keyname, value):
035|         “””Set Item. {obj[key]=val}”””
036|         print(f’setitem({keyname},{value})’)
037| 
038|         if keyname in self:
039|             raise ValueError(‘Values are read-only.’)
040|         super().__setitem__(keyname, value)
041| 
042|     def visit (self, callback_function):
043|         “””Visit each item in the bag.”””
044| 
045|         # For each key in the dictionary…
046|         for ix,key in enumerate(self.keys(), start=1):
047|             print(f’visit[{ix}]: “{key}” = “{self[key]}”‘)
048|             # Pass key and value to function…
049|             retval = callback_function(ix, key, self[key])
050|             # If return value other than None,…
051|             if retval is not None:
052|                 # Update entry with return value…
053|                 super().__setitem__(key, retval)
054| 
055|         return self
056| 
057| 
058| if __name__ == ‘__main__’:
059|     “””Exercise the test_bag.”””
060| 
061|     def print_function (ix, key, value):
062|         print(f'{ix}: {key}=”{value}” ({type(value).__name__})’)
063| 
064|     bag = test_bag(x=1.0, y=0.0, z=0.0, a=True, b=False)
065|     print()
066| 
067|     print(‘Visit…’)
068|     bag.visit(print_function)
069|     print()
070| 

Line #1 begins the definition of a new class, test_bag that subclasses dict. (I often refer to essentially unordered associative collections as “bags”. I think of them as opaque bags of any sorts of things accessed by identifying keys.)

Lines #3 to #9 implement the built-in __new__ method for the test_bag class. Because the dict isn’t populated until the __init__ method, we don’t need to intercept the new object process. We’re not doing anything necessary here. The only reason we implement it is to instrument the new process with the two print statements (line #5 and #8). Note that we must be sure to call the superclass (line #7) and return the object instance it creates (line #9).

Lines #11 to #20 implement the built-in __init__ method for the test_bag class. Note that when we call the superclass (line #14), we do not pass the kwargs parameter. This prevents dict from populating the instance with any passed arguments. It’s in lines #16 to #20 that we populate the instance “by hand” — just so we can print each passed argument.

(See Simple Tricks #10 for more details on __new__ and __init__.)

Lines #22 to #26 implement the built-in __missing__ method. Python calls this method whenever key isn’t found in the collection. In the dict class, this raises a ValueError exception. Here we simply return None — the idea being that returning None is better than raising an exception. Obviously, a client using this class would need to check for it as a returned value.

Lines #28 to #32 implement the built-in __getitem__ method that, given a key, returns an item from the collection. (Or invokes __missing__ if not found.) Again, we’re not adding any functionality here other than the print statement for instrumentation.

Lines #34 to #40 implement the built-in __setitem__ method that, given a key and a new value, either updates (if the key exists) or adds the value (if the key does not). In our override of the method, we disallow updating of existing key=value pairs but allow new ones to be added. (This is entirely for fun/demonstration.)

Lastly, lines #42 to #55 implement a visit method that enumerates through the existing keys and calls the given function with the index, key, and value. That function can return None to signify no action or any other value to update the dict value. (Note that this method dodges the prohibition on updating values! So, we don’t entirely disallow updating values. We just make it harder.)

Lines #58 to #69 exercise the test_bag class. Note how we use the visit function to print the content of the bag.

When run (as is), this prints:

new(dict_keys(['x', 'y', 'z', 'a', 'b']))
self={}
init(dict_keys(['x', 'y', 'z', 'a', 'b']))
arg[0] x="1.0"
setitem(x,1.0)
arg[1] y="0.0"
setitem(y,0.0)
arg[2] z="0.0"
setitem(z,0.0)
arg[3] a="True"
setitem(a,True)
arg[4] b="False"
setitem(b,False)

Visit...
getitem(x)
visit[1]: "x" = "1.0"
getitem(x)
1: x="1.0" (float)
getitem(y)
visit[2]: "y" = "0.0"
getitem(y)
2: y="0.0" (float)
getitem(z)
visit[3]: "z" = "0.0"
getitem(z)
3: z="0.0" (float)
getitem(a)
visit[4]: "a" = "True"
getitem(a)
4: a="True" (bool)
getitem(b)
visit[5]: "b" = "False"
getitem(b)
5: b="False" (bool)

Note how when we print the new instance in the __new__ method, despite having passed kwargs to the superclass, the instance is not yet populated. (This output also shows that kwargs.keys() is a specific type, a dict_keys object.)

Note also that when we populate the instance, we invoke the __setitem__ method for each key=value we set. Later, when we visit the instance to print it, we invoke the __getitem__ method for each key=value pair we read.

If we comment out (or otherwise disable) the instrumentation, it just prints:

Visit...
1: x="1.0" (float)
2: y="0.0" (float)
3: z="0.0" (float)
4: a="True" (bool)
5: b="False" (bool)

It is in all regards a Python dictionary object, but it has a few special features. You can use it to experiment, or as a base class to provide instrumentation for subclasses of your own.


We’ll implement a simple “Ticket” class (or more reasonably a base class that could be extended with additional properties). We’re going to require that each ticket issued have a unique ID number and a timestamp of when it was issued. First, we need some helper classes:

001| from time import time
002| from datetime import datetime, timedelta, tzinfo
003| from random import random
004| 
005| UID = lambda: (int(1000*time()) * int((2**22)*random()))
006| 
007| class TimeZone (tzinfo):
008|     “””A TimeZone class (extending tzinfo).”””
009| 
010|     # Dates for Daylight Saving Time (DST)…
011|     DST0 = datetime(2018, 3, 11, 2, 0, 0)
012|     DST1 = datetime(2018, 11, 4, 2, 0, 0)
013| 
014|     # One hour and zero…
015|     HOUR  = timedelta(hours=1)
016|     ZERO  = timedelta(0)
017| 
018|     def utcoffset (self, dt):
019|         “””This time zone’s UTC offset.”””
020|         return self.offset + self.dst(dt)
021| 
022|     def dst (self, dt):
023|         “””Return difference for thie time zone’s DST.”””
024|         return self.HOUR if self._dst(dt) else self.ZERO
025| 
026|     def _dst (self, dt):
027|         “””Figure out if we’re in DST now.”””
028|         dst0 = self.DST0.replace(year=dt.year)
029|         dst1 = self.DST1.replace(year=dt.year)
030|         dtx =  dt.replace(tzinfo=None)
031|         return dst0 <= dtx < dst1
032| 
033| class UTC (TimeZone):
034|     “””aka GMT aka ‘Zulu Time'”””
035|     def tzname (self, dt):
036|         return ‘UTC’
037| 
038|     def utcoffset (self, dt):
039|         return self.ZERO
040| 
041|     def dst (self, dt):
042|         return self.ZERO
043| 
044| class EST (TimeZone):
045|     “””Eastern Standard Time.”””
046|     offset = timedelta(hours=5)
047| 
048|     def tzname (self, dt):
049|         return ‘EDT’ if self._dst(dt) else ‘EST’
050| 
051| class CST (TimeZone):
052|     “””Central Standard Time.”””
053|     offset = timedelta(hours=6)
054| 
055|     def tzname (self, dt):
056|         return ‘CDT’ if self._dst(dt) else ‘CST’
057| 
058| class MST (TimeZone):
059|     “””Mountain Standard Time.”””
060|     offset = timedelta(hours=7)
061| 
062|     def tzname (self, dt):
063|         return ‘MDT’ if self._dst(dt) else ‘MST’
064| 
065| class PST (TimeZone):
066|     “””Pacific Standard Time.”””
067|     offset = timedelta(hours=8)
068| 
069|     def tzname (self, dt):
070|         return ‘PDT’ if self._dst(dt) else ‘PST’
071| 
072| class HST (TimeZone):
073|     “””Hawaii Standard Time.”””
074|     offset = timedelta(hours=10)
075| 
076|     def tzname (self, dt):
077|         return ‘HDT’ if self._dst(dt) else ‘HST’
078| 

For brevity, I’m not going to go over this in any detail. The UID function just uses time and a random number to generate a unique ID number. The rest just implements a set of TimeZone classes for use with Python’s datetime class.

(Including TimeZone classes for most of the USA time zones is overkill for this project — we only need the UTC class — but I included them in case you find them useful)

((Even so, my apologies for not including Alaska time.))


Now we can implement our ticket class:

001| from datetime import datetime
002| from examples import UID, UTC
003| 
004| class ticket (dict):
005|     “””Implementing a Ticket by subclassing dict.”””
006| 
007|     def __init__ (self, firstname, lastname, **kwargs):
008|         “””New tickets require a first and last name.”””
009|         super().__init__(**kwargs)
010|         self[‘tid’] = UID()
011|         self[‘ts’] = datetime.now(tz=UTC())
012|         self[‘firstname’] = firstname
013|         self[‘lastname’] = lastname
014| 
015|     @property
016|     def tid (self): return self[‘tid’]
017| 
018|     @property
019|     def name (self): return f'{self[“firstname”]} {self[“lastname”]}’
020| 
021|     @property
022|     def timestamp (self): return self[‘ts’].strftime(‘%Y-%m-%d %H:%M:%S’)
023| 
024|     def __missing__ (self, key): return ‘<null>’
025| 
026|     def __eq__ (self, other): return (self[‘ts’] == other[‘ts’])
027|     def __ne__ (self, other): return (self[‘ts’] != other[‘ts’])
028|     def __le__ (self, other): return (self[‘ts’] <= other[‘ts’])
029|     def __lt__ (self, other): return (self[‘ts’] <  other[‘ts’])
030|     def __gt__ (self, other): return (self[‘ts’] >  other[‘ts’])
031|     def __ge__ (self, other): return (self[‘ts’] >= other[‘ts’])
032| 
033|     def __hash__ (self): return hash(self[‘tid’])
034| 
035|     def __setitem__ (self, keyname, value):
036|         “””Set Item.”””
037|         if keyname in self:
038|             raise ValueError(‘Entries are read-only.’)
039|         super().__setitem__(keyname, value)
040| 
041| 
042|     def __delitem__ (self, keyname):
043|         “””Delete Item.”””
044|         raise ValueError(‘Entries may not be deleted.’)
045| 
046|     def __str__ (self):
047|         return f'[{self.timestamp}] {self.name} ({self.tid})’
048| 
049|     def __repr__ (self):
050|         t = (self.tid, self.timestamp, self[‘firstname’], self[‘lastname’])
051|         s = ‘{ticket:{tid:%s, ts:”%s”, firstname:”%s”, lastname:”%s”}}’
052|         return s%t
053| 
054| 
055| if __name__ == ‘__main__’:
056|     “””Exercise the ticket class.”””
057|     print()
058| 
059|     tick = ticket(‘Wyrd’,‘Smythe’, job=‘123456-78’, due=‘2025-08-14’)
060|     print(f'{tick!s}’)
061|     print()
062| 

This is fairly straight-forward, but there are a few wrinkles, some of which should be familiar from the previous example. For instance, we again implement the __missing__ method (line #24), so clients need to watch for the '<null>' string value rather than an exception.

Ticket entries are intended to be immutable, so we implement __setitem__ (lines #35 to #39) and __delitem__ (lines #42 to #44) to prevent updating and removing them. As before, we do allow adding new entries.

This time there’s no visitor method that allows easily getting around our restrictions, so we’ll treat instances of this class as immutable. We implement the __hash__ method (line #33) to allow their use as keys.

We also implement the relational operators (lines #26 to #31) to make these objects sortable.

Most importantly, the __init__ method (lines #7 to #13) takes explicit first and last name arguments in addition to a set of keyword arguments (which will be added to the dictionary). The names, along with a generated ID and timestamp, are added to the dictionary with specific names.

We define various properties (lines #15 to #22) for convenience (note their use in the __str__ and __repr__ methods).

When run, this just prints:

[2025-07-20 18:07:25] Wyrd Smythe (4547523151084949552)

(The ID number is different each time run because of the random function. The timestamp, of course, also varies each time run.)

We can test the immutability with the following:

001| from demos import ticket
002| 
003| if __name__ == ‘__main__’:
004|     “””Exercise the ticket class.”””
005| 
006|     # Create a new ticket…
007|     tick = ticket(‘Wyrd’,‘Smythe’, job=‘123456-78’, status=‘new’)
008|     print(tick)
009|     print()
010| 
011|     # Try to update an entry…
012|     print(f’status1 = “{tick[“status”]}”‘)
013|     try:
014|         tick[‘status’] = ‘accepted’
015|     except Exception as e:
016|         print(e)
017|         print()
018| 
019|     # Try to delete an entry…
020|     try:
021|         del tick[‘status’]
022|     except Exception as e:
023|         print(e)
024|         print()
025| 
026|     # Add a new entry…
027|     try:
028|         tick[‘status2’] = ‘open’
029|         print(f’status2 = “{tick[“status2”]}”‘)
030|         print()
031|     except Exception as e:
032|         print(e)
033|         print()
034| 
035|     # Try to update the new entry…
036|     try:
037|         tick[‘status2’] = ‘closed’
038|     except Exception as e:
039|         print(e)
040|         print()
041| 
042|     # Use the dict base class to modify entries…
043|     super(type(tick), tick).__setitem__(‘status’, ‘accepted’)
044|     super(type(tick), tick).__setitem__(‘status2’, ‘closed’)
045|     print(f’status1 = “{tick[“status”]}”‘)
046|     print(f’status2 = “{tick[“status2”]}”‘)
047|     print()
048| 

When run, this prints:

[2025-07-20 18:07:39] Wyrd Smythe (4827626601178243512)

status1 = "new"
Entries are read-only.

Entries may not be deleted.

status2 = "open"

Entries are read-only.

status1 = "accepted"
status2 = "closed"

Instances don’t allow their entries to be modified or deleted. At least not easily. But we can always appeal to the dict base class as we did in line #43 and #44.

So, it’s not impossible to modify values, but since only the ID is used to generate a hash, it’s the only entry that absolutely must be immutable. If this was a serious class rather than an illustration, we’d want to take steps to protect that entry.

We can test sorting with this bit of code:

001| from time import sleep
002| from demos import ticket
003| 
004| if __name__ == ‘__main__’:
005|     “””Exercise the ticket class.”””
006| 
007|     t1 = ticket(‘Wyrd’,‘Smythe’, job=‘123456-78’)
008|     print(t1)
009|     sleep(3)
010| 
011|     t2 = ticket(‘Fred’, ‘Flintstone’, job=‘123789-34’)
012|     print(t2)
013|     sleep(3)
014| 
015|     t3 = ticket(‘Barney’, ‘Rubble’, job=‘567258-42’)
016|     print(t3)
017|     print()
018| 
019|     print(f'{t1 < t2 = }’)
020|     print(f'{t2 < t3 = }’)
021|     print()
022| 
023|     for ix,tick in enumerate(sorted([t3,t1,t2]), start=1):
024|         print(f'{ix}: {tick.name}’)
025|     print()
026| 

Which, when run, prints:

[2025-07-20 18:50:35] Wyrd Smythe (7214965656540271146)
[2025-07-20 18:50:38] Fred Flintstone (6708916351415752640)
[2025-07-20 18:50:41] Barney Rubble (4272416954490912370)

t1 < t2 = True
t2 < t3 = True

1: Wyrd Smythe
2: Fred Flintstone
3: Barney Rubble

The ordering is determined by the timestamp, so I used the sleep function to provide some time separation between tickets.


Here’s a subclass of dict that implements a list of files for a given subdirectory:

001| from os import path, listdir
002| 
003| class filelist (dict):
004|     “””Load a dictionary with a list of files from a directory.”””
005| 
006|     def __init__ (self, filepath):
007|         “””New filelist. Loads data dynamically.”””
008|         super().__init__()
009|         self.fpath = filepath
010| 
011|         # Add files…
012|         for name in listdir(filepath):
013|             fn = path.join(self.fpath, name)
014| 
015|             # It’s a file…
016|             if path.isfile(fn):
017|                 self[name] = {
018|                     ‘@’:1,
019|                     ‘name’:fn,
020|                     ‘size’:path.getsize(fn),
021|                     ‘dlm’:path.getmtime(fn),
022|                     ‘dcr’:path.getctime(fn),
023|                 }
024|                 continue
025| 
026|             # It’s a subdirectory…
027|             if path.isdir(fn):
028|                 self[name] = {
029|                     ‘@’:2,
030|                     ‘name’:fn
031|                 }
032|                 continue
033| 
034|             # Not a file or a subdirectory…
035|             raise RuntimeError(f’Unknown directory type: {name}’)
036| 
037|     def __repr__ (self):
038|         return f'{self.fpath} ({len(self)})’
039| 
040| 
041| if __name__ == ‘__main__’:
042|     “””Exercise the filelist class.”””
043| 
044|     # New filelist instance…
045|     flist = filelist(r’C:\demo\hcc\python’)
046|     print(flist)
047| 
048|     # Sort key…
049|     sort_by_type = lambda k:flist[k][‘@’]
050| 
051|     # List the files…
052|     for name in sorted(flist, key=sort_by_type, reverse=True):
053|         rcd = flist[name]
054|         match rcd[‘@’]:
055| 
056|             # Files…
057|             case 1:
058|                 size = f'{rcd[“size”]:,}’
059|                 print(f'{name:<24s}{size:>8s} bytes’)
060| 
061|             # Subdirectories…
062|             case 2:
063|                 print(f'{name:24s}(subdirectory)’)
064| 
065|     print()
066| 

Here we only need to implement the __init__ method (lines #6 to #35) and __repr__ method (lines #37 and #38). (The latter just because we always implement toString.)

An instance requires a directory name which it uses to generate a list of files and subdirectories in that directory (lines #12 to #35). The routine is not recursive and does not scan any subdirectories. I’ll leave that as a user exercise.

The dictionary keys are the file and subdirectory names (with no path info). The dictionary values are dictionaries with two guaranteed entries, “@” and “name”. The first is either a 1 (this entry is a file) or 2 (this entry is a subdirectory). The “name” entry is the entire path and file name.

If the entry is a file (“@” = 1), then there are three additional entries: “size” (the size of the file in bytes), “dcr” (the date the file was created), and “dlm” (the date the file was last modified).

When run, this prints:

C:\demo\hcc\python (13)
config                  (subdirectory)
images                  (subdirectory)
inputs                  (subdirectory)
outputs                 (subdirectory)
2b-or-not2b.txt              450 bytes
chriscarol.txt             5,596 bytes
colors.png                 1,992 bytes
configs.json                 675 bytes
configuration.ini          1,099 bytes
example.json               1,040 bytes
example.xml                1,373 bytes
randnums.dat              19,164 bytes
vim.txt                      334 bytes

Lastly, here’s a subclass of dict that reads a Windows™ INI file and generates a dictionary of the sections and values found:

001| class inifile (dict):
002| 
003|     def __init__ (self, filename, encoding=‘utf8’):
004|         “””New inifile. Loads data dynamically.”””
005|         super().__init__()
006|         self.fname = filename
007| 
008|         # Open, read, and parse the INI file…
009|         fp = open(filename, mode=‘r’, encoding=encoding)
010|         try:
011|             print(f’reading: {filename}…’)
012|             print()
013|             sect_name = ‘main’
014|             self[sect_name] = {}
015| 
016|             for ix,line in enumerate(fp, start=1):
017|                 line = line.strip()
018|                 #print(f'{ix:2d}: {line}’)
019| 
020|                 # Ignore blank lines…
021|                 if len(line) == 0:
022|                     continue
023| 
024|                 # Ignore comments…
025|                 if line.startswith(‘#’) or line.startswith(‘;’):
026|                     continue
027| 
028|                 # Process section names…
029|                 if line.startswith(‘[‘):
030|                     # New section name…
031|                     if line[1] != ‘]’:
032|                         SyntaxError(f’Invalid section header: “{line}”‘)
033|                     sect_name = line[1:1].strip()
034|                     self[sect_name] = {}
035|                     continue
036| 
037|                 # Name and value pair for current section…
038|                 parts = line.split(‘=’, maxsplit=1)
039|                 name = parts[0].strip()
040|                 valu = parts[1].strip() if 1 < len(parts) else 
041|                 self[sect_name][name] = valu
042| 
043|             print()
044|         except:
045|             raise
046|         finally:
047|             fp.close()
048| 
049|     def __repr__ (self):
050|         return f'{self.fname} ({len(self)} sections)’
051| 
052| 
053| if __name__ == ‘__main__’:
054|     “””Exercise the inifile class.”””
055| 
056|     # New INI file dictionary…
057|     ini = inifile(r”C:\demo\hcc\python\configuration.ini”)
058|     print(f’INI File: {ini.fname}’)
059| 
060|     # Iterate through the sections and list name=value pairs…
061|     for sect in ini:
062|         print(f’Section: “{sect}”‘)
063|         for ix,name in enumerate(ini[sect], start=1):
064|             print(f'{ix:2d}: {name}={ini[sect][name]}’)
065|         print()
066|     print()
067| 

Again, we only need to implement the __init__ and __repr__ methods. In the former, we open, read, and parse the INI file text (lines #9 to #41).

The INI file for the test looks like this:

# Configuration file
#
Python=C:\Python310\python.exe
BasePath=C:\demo\hcc\python

index = modules.list
version = 1.0.0.0

[colors ]
red   = #ff0000
green = #007f00
blue  = #0000ff

[ paths ]
"C:/demo/hcc/python/baseball"
"C:/demo/hcc/python/baseball/etc"
"C:/demo/hcc/python/baseball/ftp"
"C:/demo/hcc/python/baseball/xslt"

 [modules]
gameday
gameday_url
gameday_msb
gameday_boxscore
gameday_linescore
gameday_innings
gameday_game
gameday_players


[]
/eof

Note that the section names are designed to stress-test the parser. We expect the names to be “colors” and “paths” with no spaces. And we ignore spaces before or after the section name brackets. Likewise, leading and trailing spaces, as well as spaces on either side of the equals sign, are ignored in the name=value pairs.

When we run the above code, it prints:

reading: C:\demo\hcc\python\configuration.ini...

INI File: C:\demo\hcc\python\configuration.ini
Section: "main"
 1: Python=C:\Python310\python.exe
 2: BasePath=C:\demo\hcc\python
 3: index=modules.list
 4: version=1.0.0.0

Section: "colors"
 1: red=#ff0000
 2: green=#007f00
 3: blue=#0000ff

Section: "paths"
 1: "C:/demo/hcc/python/baseball"=
 2: "C:/demo/hcc/python/baseball/etc"=
 3: "C:/demo/hcc/python/baseball/ftp"=
 4: "C:/demo/hcc/python/baseball/xslt"=

Section: "modules"
 1: gameday=
 2: gameday_url=
 3: gameday_msb=
 4: gameday_boxscore=
 5: gameday_linescore=
 6: gameday_innings=
 7: gameday_game=
 8: gameday_players=

Section: ""
 1: /eof=

You can extend this (or the classes above) to give them more features. For instance, you could make the INI file reader type-aware to make the values be integers or strings or dates or whatever.


I hope these examples show some of the power of subclassing the built-in types like dict or list. Using full-featured, well-tested classes as a starting point gives your extended classes a lot of power. All the subclasses above inherit the keys, values, and items methods of dict, for example.


Link: Zip file containing all code fragments used in this post.