[Tutor] Good Taste Question: Using SQLite3 in Python

Peter Otten __peter__ at web.de
Wed Apr 29 10:50:32 CEST 2015


Jugurtha Hadjar wrote:

> I have a class with methods that access a database (SQLite3). I have 
> included an excerpt showin reading and writing and would like to know if 
> I'm doing it right. (i.e: Is it bad code and what to improve).
> 
> Here are some improvements and my rationale (check my thinking):
> 
> - Initially, each method had its SQL statement(s) inside, but I grouped 
> all statements in a dictionary, with operations as keys, as a class 
> 'constant' as per previous advice on this mailing list.
> 
> - Each method used sqlite3 module on its own, but it was repetitive so I 
> put that part in its own method `init_db` that returns a tuple 
> consisting in a connection and a cursor.
> 
> - Sometimes there was an exception raised, so I used `try` in `init_db`.
> 
> - Methods closed the connection themselves, so I used `with` in 
> `init_db` instead of `try`, as it would close the connection 
> automatically and rollback (I hope I'm not making this up).
> 
> Here's the excerpt (`DB_FILES` and `QUERIES` are not included here for 
> more clarity).
> 
> Thank you.
> 
> 
> 
>         def __init__(self, phone):
> 
>                 # Get preliminary information on user and make them
>                 # available.
> 
>                 self.phone = phone
>                 self.known = self.find()
>                 
>                 if self.known:
>                         self.balance = self.get_balance()
>                 else:
>>                         self.balance = None
> 
>         def init_db(self):
>                 with sqlite3.connect(self.DB_FILE) as conn:
>                         return conn, conn.cursor()
> 
>         def find(self):
>                 '''Find the phone in the users database.'''
>                 
>                 (__, cursor) = self.init_db()
>                 try:
>                         cursor.execute(
>                                 self.QUERIES['FIND_PHONE'],
>                                 (self.phone,)
>                         )
>                         found = cursor.fetchone()
>                         return True if found else False
>                 except Exception as e:
>                         return self.ERROR.format(e.args[0])
> 
>         def create(self, seed_balance):
>                 ''' Create a database entry for the sender.'''
> 
>                 conn, cursor = self.init_db()
>                 try:
>                         cursor.execute(
>                                 self.QUERIES['CREATE'],
>                                 (self.phone, seed_balan>ce)
>                         )
>                         conn.commit()
>                 except Exception as e:
>                         return self.ERROR.format(e.args[0])

My random observations:

(1) find() and create() look very similar. Try hard to factor out common 
code. You might even combine it into a single method ensure_balance(phone).

(2) According to

https://docs.python.org/dev/library/sqlite3.html#using-the-connection-as-a-context-manager

- the context manager automatically commits
- you can run sql directly on the connection

It might by a good idea to refactor init_db() to just return the connection 
and then use it as

    with self.init_db() as conn:
        return conn.execute(
            self.QUERIES['FIND_PHONE'], 
            (self.phone,)).fetchone() is not None

If you need a cursor you can easily get one from the connection. If you want 
more complex handling later you can always turn init_db() into a 
contextmanager (see
<https://docs.python.org/dev/library/contextlib.html#contextlib.contextmanager> 
). 

(3) Catching Exception is overly broad. You will also catch a typo like

cursor.execute(
    self.QUERIES['CERATE'],
    (self.phone, seed_balance)
)

where the proper fix is to modify the script. Only catch exceptions that you 
can actually handle. Example: a table doesn't exist, and you want to create 
it lazily on first access. Let all other exceptions just bubble up. It may 
seem lazy, but a complete traceback is invaluable for identifying and fixing 
a bug.

(4) self.ERROR.format(e.args[0])

is probably a string with len() > 0, and thus True in a boolean context. 
>From that follows an exception in the find() method in

>                 self.known = self.find()

causes the following if-suite to be run

>                 if self.known:
>                         self.balance = self.get_balance()

and if get_balance() has a same idea of proper exception handling

self.balance will end up containing an error message.

(5) From point (4) I conclude that you don't have unit tests that cover your 
code. You should really write some of these before pondering about stylistic 
issues. Unit tests 

- help avoid errors
- make changes in the code less of a gamble because if the tests succeed 
after the change you can be confident the change didn't introduce an error
- what you might not expect: how drastically tests affect the style of your 
code. You'll see yourself thinking "How can I test that?" with every line of 
code that you write, and that alone will improve the quality of your code.

A simple example is your self.find(). How would you test that with different 
phone numbers? That's easier when you can pass the phone number as an 
argument, so you might change the method's signature.





More information about the Tutor mailing list