静的型付け#

psycopg のソースコードは、PEP 0484 の型ヒントに従ってアノテーションされていて、Mypy の現在のバージョンで --strict モードを使用して型チェックされています。

アプリケーションが Mypy を使用して型チェックされている場合、psycopg の型を活用して psycopg オブジェクトとデータベースから返されたデータが正しく使用されているかを検証できます。

ジェネリック型#

psycopg の ConnectionCursor オブジェクトは Generic オブジェクトであり、返されたレコードの型である Row パラメータをサポートしています。

デフォルトでは、Cursor.fetchall() などのメソッドは、サイズとコンテンツが未知の通常のタプルを返します。したがって、connect() 関数は型 psycopg.Connection[Tuple[Any, ...]] のオブジェクトを返し、Connection.cursor() は型 psycopg.Cursor[Tuple[Any, ...]] のオブジェクトを返します。ジェネリックな配管コード (訳注: データベースに接続するコード) を書いている場合は、Connection[Any]Cursor[Any] などのアノテーションを使うのが実用的かもしれません。

conn = psycopg.connect() # 型は psycopg.Connection[Tuple[Any, ...]] です

cur = conn.cursor()      # 型は psycopg.Cursor[Tuple[Any, ...]] です

rec = cur.fetchone()     # 型は Optional[Tuple[Any, ...]] です

recs = cur.fetchall()    # 型は List[Tuple[Any, ...]] です

返された行の型#

データを異なる型、たとえばディクショナリとして返すコネクションとカーソルを使用したい場合、connect()cursor() メソッドの row_factory 引数が使えます。この引数は、カーソルの fetch メソッドから返されるレコードの型をコントロールして、返されたオブジェクトを適切にアノテートします。詳細については 行ファクトリ を参照してください。

dconn = psycopg.connect(row_factory=dict_row)
# dconn の型は psycopg.Connection[Dict[str, Any]] です

dcur = conn.cursor(row_factory=dict_row)
dcur = dconn.cursor()
# dcur の型はいずれの場合も psycopg.Cursor[Dict[str, Any]] です

drec = dcur.fetchone()
# drec の型は Optional[Dict[str, Any]] です

例: レコードを Pydantic モデルとして返す#

Pydantic を使用すると、ランタイム時の静的型付けの矯正が可能になります。Pydantic モデル ファクトリ を使用すると、Mypy を使用してコードの静的な型チェックができ、返された行がモデルと互換性がない場合にデータベースのクエリが例外を起こすようになります。

次の例では、mypy --strict を使用して問題のレポートなしで型チェックができています。Pydantic は、互換性のないデータを返すクエリで Person が使用された場合に、ランタイム時のエラーも起こします。

from datetime import date
from typing import Optional

import psycopg
from psycopg.rows import class_row
from pydantic import BaseModel

class Person(BaseModel):
    id: int
    first_name: str
    last_name: str
    dob: Optional[date]

def fetch_person(id: int) -> Person:
    with psycopg.connect() as conn:
        with conn.cursor(row_factory=class_row(Person)) as cur:
            cur.execute(
                """
                SELECT id, first_name, last_name, dob
                FROM (VALUES
                    (1, 'John', 'Doe', '2000-01-01'::date),
                    (2, 'Jane', 'White', NULL)
                ) AS data (id, first_name, last_name, dob)
                WHERE id = %(id)s;
                """,
                {"id": id},
            )
            obj = cur.fetchone()

            # ここで reveal_type(obj) は 'Optional[Person]' を返すはずです

            if not obj:
                raise KeyError(f"person {id} not found")

            # ここで reveal_type(obj) は 'Person' を返すはずです

            return obj

for id in [1, 2]:
    p = fetch_person(id)
    if p.dob:
            print(f"{p.first_name} さんは {p.dob.year} に生まれました")
        else:
            print(f"誰も {p.first_name} さんがいつ生まれたのかを知りません")

クエリ内のリテラル文字列の型チェック#

execute() や類似のメソッドのようなメソッドは、PEP 675 によれば、リテラル文字列のみを入力として受け取る必要があります。つまり、クエリは任意の文字列式ではなく、コード中のリテラル文字列から来る必要があるということです。

たとえば、クエリへの引数の引き渡しは execute() の2つ目の引数を経由で行なわれる必要があり、文字列の構築によって行われてはいけません。

def get_record(conn: psycopg.Connection[Any], id: int) -> Any:
    cur = conn.execute("SELECT * FROM my_table WHERE id = %s" % id)  # BAD!
    return cur.fetchone()

# この関数は、以下のように実装する必要があります

def get_record(conn: psycopg.Connection[Any], id: int) -> Any:
    cur = conn.execute("select * FROM my_table WHERE id = %s", (id,))
    return cur.fetchone()

クエリを動的に構築する場合、sql.SQL と同様のオブジェクトを使用して、テーブルとフィールドの名前を安全にエスケープする必要があります。SQL() オブジェクトのパラメータはリテラル文字列である必要があります。

def count_records(conn: psycopg.Connection[Any], table: str) -> int:
    query = "SELECT count(*) FROM %s" % table  # BAD!
    return conn.execute(query).fetchone()[0]

# この関数は、以下のように実装する必要があります

def count_records(conn: psycopg.Connection[Any], table: str) -> int:
    query = sql.SQL("SELECT count(*) FROM {}").format(sql.Identifier(table))
    return conn.execute(query).fetchone()[0]

これを書いている時点では、この型チェックを実装している Python の静的型チェッカーは存在しません (mypy はこれを実装していませんPyre は実装していますが、psycopg ではまだ機能しません)。型チェッカーのサポートが完了したら、上記の悪いステートメントはエラーとして報告されるようになるはずです。