Python programming book

Reflection of class attributes in Python

I’ll admit that, at the time of this writing, I was just starting to code with Python. Yet, I wanted to apply clean code as I’ve learned it.

The project

I wanted to get familiar with Python and building a simple time logger API was something that I wanted to do.

The app is organized into projects. Projects have tasks and each time we work on a task, we record the time spent.

Simple, isn’t it?

My use case

I’ll skip ahead to the moment I wanted to save to a SQL Lite database a project and read a project by id or the list of projects in the database.

I used Gemini all the way to code the API skeleton.

About AI to learn coding...

AI doesn’t do everything for you and I find that it won’t find nor code the solution for you 100% of the time.

Though it’s definitely helpful, you’ll need skills like the fundamentals in the programming language.

So if you start with fundamental questions, you may understand better the more complex questions.

When it was time to implement the get_project method, the AI gave this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def get_project(project_id: str) -> ProjectData | None:
  conn = sqlite3.connect(f'db{os.sep}database_sqllite3.db')
  cursor = conn.cursor()

  cursor.execute('''SELECT id, name, color, isArchived FROM projects WHERE id = ?''', (project_id,))
  project_data = cursor.fetchone()

  conn.close()

  if project_data:
    return ProjectData(project_data[1], project_data[2], bool(project_data[3]))  # Convert retrieved data to ProjectData object
  else:
    return None

I didn’t like the index-based parsing of the project_data record.

So I asked how to make the code be more generic so it uses the DTO class attributes to build the response.

The AI suggested two approaches:

1. Using namedtuples: it explained “This approach provides a more readable way to access the data by using named fields instead of numerical indexes.”

2. Using dictionary comprehension: it explained “This approach offers a more concise way to convert the data into a dictionary, especially if you need to handle dynamic column names.”

I went for the namedtuples way and the code looked like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from collections import namedtuple

ProjectRecord = namedtuple('ProjectRecord', ['id', 'name', 'color', 'isArchived'])

def get_project(project_id: str) -> Optional[ProjectRecord]:
  # ... (same connection and cursor logic)

  cursor.execute('''SELECT id, name, color, isArchived FROM projects WHERE id = ?''', (project_id,))
  project_data = cursor.fetchone()

  conn.close()

  if project_data:
    return ProjectRecord(*project_data)  # Unpack data using * operator
  else:
    return None

Though it read the database fine and return the data, the API responded an array of value instead of an object:

1
["6964ba78-8dc4-4f23-b525-a0d9fbab865c", "New Project", "#000000", 0]

I needed the output to be:

1
2
3
4
5
6
{
  "id": "6964ba78-8dc4-4f23-b525-a0d9fbab865c",
  "name": "New Project",
  "color": "#000000",
  "isArchived": false
}

After a few prompts, the AI gave me the _asdict method that is provided by namedtuples. It creates a dictionary with keys matching the named fields.

So the final code on the API controller looked like this:

1
2
3
4
5
6
7
@app.route('/api/v1.0/project/<string:id>', methods=['GET'])
def api_project_get(id: int):
  project = get_project(id)
  if project:
    return jsonify(project._asdict())
  else:
    return jsonify({'error': 'Project not found'}), 404

More reusability

I wanted to go further into reusability.

The AI gave the following as the starting code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def get_namedtuple_from_type(cls):
  """
  Creates a namedtuple type based on the public attributes of a class.

  Args:
      cls (class): The class to use for generating the namedtuple type.

  Returns:
      namedtuple: A namedtuple type with fields matching the class's public attributes.
  """
  field_names = [attr for attr, value in getmembers(cls) if not callable(value)]
  tuple = namedtuple(cls.__name__, field_names)
  return tuple

PS: I renamed the method name and returned value to be more generic.

Unfortunately, it didn’t work and even if I tried a few other prompts, the AI couldn’t get me a working solution.

So I went to a Google search and I realized something.

In the scaffolding stage of the API, the AI suggested me to use a class to define a project.

The AI gave me this:

1
2
3
4
5
6
class ProjectData:
  def __init__(self, name: str, color: str, isArchived: bool = False):
    self.id = None  # This will be assigned during project creation
    self.name = name
    self.color = color
    self.isArchived = isArchived

At that moment, I realized that the attributes of ProjectDto weren’t id, name, color and isArchived… So I couldn’t get ['id', 'name', 'color', 'isArchived']

So I added the class attributes:

1
2
3
4
5
class ProjectData:
  id: str | None
  name: str
  color: str
  isArchived: bool

With a bit of debugging, I saw that to get the attributes, I had to do the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def get_tuple_from_type(cls):
  # The line below instantiate a dummy object to filter out all the default members
  # of an object instance.
  boring = dir(type('dummy', (object,), {}))
  cls_extract = [item
            for item in getmembers(cls)
            if item[0] not in boring]
  # I was left with one item in cls_extract = ['__annotations__']
  # There is no reason that I don't get at least one item in cls_extract
  if cls_extract.__len__ == 0:
    raise TypeError('(Lvl1) The class has no attribute defined. Cannot use "get_tuple_from_type".')
  if cls_extract[0].__len__ == 0:
    raise TypeError('(Lvl2) The class has no attribute defined since "__annotations__" wasn´t found. Cannot use "get_tuple_from_type".')
  # This makes sure I get a runtime error in case the Dto class doesn't declare attributes
  if cls_extract[0][0] != '__annotations__':
    raise TypeError('The class has no attribute defined since "__annotations__" wasn´t found. Cannot use "get_tuple_from_type".')

  # The first element of cls_extract is an array itself where:
  #   - index=0 equals to an array
  #   - index=1 equals to an object with the cls type's attributes
  # So below, I retrieve the keys from the object
  raw_field_names = cls_extract[0][1].keys()
  # and then convert it to an array of strings.
  field_names = list(raw_field_names)
  # At last, we have the namedtuple!
  return namedtuple(cls.__name__, field_names)

So, my get_project method became the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from dto.ProjectData import ProjectDto

def get_project(project_id: str) -> ProjectDto:
  try:
    conn = sqlite3.connect(f'db{os.sep}database_sqllite3.db')
    cursor = conn.cursor()
    cursor.execute('''SELECT id, name, color, isArchived FROM projects WHERE id = ?''', (project_id,))
    project_data = cursor.fetchone()
    ProjectRecord = get_tuple_from_type(ProjectDto)  # Get namedtuple type dynamically

    if project_data:
      return ProjectRecord(*project_data)  # Unpack data using * operator
    else:
      return None

  finally:
    conn.close()

Thanks to that approach, the get_projects was very simple to write. I could therefore reuse get_tuple_from_type and return the list of projects:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def get_projects() -> list[ProjectDto]:
  try:
    conn = sqlite3.connect(f'db{os.sep}database_sqllite3.db')
    cursor = conn.cursor()

    cursor.execute('''SELECT id, name, color, isArchived FROM projects''')
    project_data = cursor.fetchall()

    ProjectRecord = get_tuple_from_type(ProjectDto)  # Get namedtuple type dynamically

    projects = []
    for row in project_data:
      projects.append(ProjectRecord(*row))  # Unpack data using * operator for each row
    return projects
  finally:
    conn.close()

Though the AI helped me scaffold the API, you still need to think and research yourself!

About the solution I coded and shared here.

I’m still learning Python and since I wrote the above, a more experienced colleague told me that using _asdict(), __len__ or __name__ isn’t safe.

All, the DTO classes should be data classes (using the @dataclasse decorator).

Finally, there is an automapper package that could perform the job in Python. I may look into this later on.

I hope you learn a few things here. I know I did.

Feel free to contact me if you see any error as I’m at the very early stage of my Python journey.

Credit: Photo by Christina Morillo.

Licensed under CC BY-NC-SA 4.0
License GPLv3 | Terms
Built with Hugo
Theme Stack designed by Jimmy