fullmetalcopy
Fast PostgreSQL bulk loads from CSV using the server-side copy path, wired through SQLAlchemy 2 so you keep your normal engines and connection patterns.
PyPI: fullmetalcopy · Source: GitHub
Why use this?
Bulk-inserting rows with plain INSERT is slow for large datasets. This library streams CSV bytes into a table via PostgreSQL’s efficient copy-style ingestion, with thin wrappers over psycopg3, psycopg2, or asyncpg depending on your SQLAlchemy URL.
Requirements
| Python | 3.10+ |
| Database | PostgreSQL only (checked at runtime) |
| Core | SQLAlchemy ≥ 2.0 |
Install at least one driver extra (or bring your own compatible stack):
| Extra | Use case |
|---|---|
psycopg |
Sync or async with psycopg v3 (postgresql+psycopg://…, postgresql+psycopg_async://…) |
psycopg2 |
Sync with psycopg2 (postgresql+psycopg2://…) |
asyncpg |
Async with asyncpg (postgresql+asyncpg://…) |
pandas / polars |
Optional; same extras as declared in pyproject.toml if you combine with dataframe tooling |
Install
pip install fullmetalcopy
# Drivers (pick what matches your engine URL)
pip install "fullmetalcopy[psycopg]"
pip install "fullmetalcopy[psycopg2]"
pip install "fullmetalcopy[asyncpg]"
# Examples with dataframe extras
pip install "fullmetalcopy[psycopg,pandas]"
pip install "fullmetalcopy[asyncpg,polars]"
Public API
Import from the package root:
import fullmetalcopy as fc
fc.__version__ # package version string
fc.copy_from_csv # synchronous
fc.async_copy_from_csv # asynchronous (alias of the async module’s copy_from_csv)
Both callables accept a binary CSV stream (io.BytesIO, open(..., "rb"), or any typing.BinaryIO).
| Argument | Default | Meaning |
|---|---|---|
connection / async_connection |
— | SQLAlchemy Connection or AsyncConnection |
csv_file |
— | Binary stream positioned at the start of the CSV payload |
table_name |
— | Target table name (unqualified unless you use schema) |
sep |
"," |
Field delimiter |
null |
"" |
See CSV and null semantics below |
columns |
None |
Optional explicit column list (order must match CSV columns after the header is applied) |
headers |
True |
If True, the first line is treated as a header row and skipped from data (column names may still be taken from the file or from columns) |
schema |
None |
PostgreSQL schema name; search_path is not permanently changed |
Driver is inferred from connection.dialect.driver. Non-PostgreSQL dialects raise a clear error.
Usage examples
Synchronous (psycopg or psycopg2)
import io
import sqlalchemy as sa
import fullmetalcopy as fc
engine = sa.create_engine("postgresql+psycopg2://scott:tiger@hostname/dbname")
with io.BytesIO() as buf:
buf.write(b"id,name\n1,Ada\n")
buf.seek(0)
with engine.connect() as conn:
fc.copy_from_csv(conn, buf, "people")
conn.commit()
Asynchronous (asyncpg or psycopg)
import io
from sqlalchemy.ext.asyncio import create_async_engine
import fullmetalcopy as fc
async def main() -> None:
engine = create_async_engine("postgresql+asyncpg://scott:tiger@hostname/dbname")
try:
async with engine.connect() as conn:
with io.BytesIO() as buf:
buf.write(b"id,name\n1,Ada\n")
buf.seek(0)
await fc.async_copy_from_csv(conn, buf, "people")
await conn.commit()
finally:
await engine.dispose()
Non-default schema
fc.copy_from_csv(conn, buf, "invoices", schema="billing")
await fc.async_copy_from_csv(conn, buf, "invoices", schema="billing")
Header row plus explicit column order
When the file has a header but you want to pin the column list (and skip the header line the same way on every backend):
fc.copy_from_csv(
conn,
buf,
"people",
columns=["id", "name"],
headers=True,
)
Driver behavior (summary)
| Backend | Sync | Async | Notes |
|---|---|---|---|
| psycopg v3 | Yes | Yes | Rows are read with Python’s csv module and sent with copy.write_row; column count must match. |
| psycopg2 | Yes | No | Uses cursor.copy_from. For schema=…, uses SET LOCAL search_path then an unqualified table name (reliable with COPY). |
| asyncpg | No | Yes | Uses copy_to_table. Header skipping is aligned with other backends (adapt_names); asyncpg is called with header=False after any header line is consumed. |
For full detail, see the docstrings in fullmetalcopy.synchronous.copycsv and fullmetalcopy.asynchronous.copycsv, and the per-driver modules under fullmetalcopy.synchronous / fullmetalcopy.asynchronous.
CSV and null semantics
- The header line (when
headers=True) is decoded as UTF-8 and parsed withcsv.reader, so quoted fields in the header are handled like normal CSV. - On the psycopg3 path, each data row is also read with
csv.readerand must have exactly as many fields as the resolved column list, or aValueErroris raised. - On the psycopg3 path, any cell that equals the
nullstring is stored as SQLNULL. The defaultnull=""maps empty fields toNULL. If you need to keep empty strings as empty strings, setnullto a sentinel that does not appear in real data (for example"\N"where appropriate).
PostgreSQL’s CSV rules and Python’s csv module are very close but not identical for exotic cases (embedded newlines, unusual escapes). For maximum fidelity on huge files, consider a raw server-side COPY … FROM STDIN pipeline; this library targets typical spreadsheet-style CSV.
Development
From a clone of the repository:
pip install -e ".[dev]"
ruff format .
ruff check .
mypy fullmetalcopy
pytest
[dev] pulls all supported drivers, pytest, Ruff, Mypy, and testing.postgresql for the test suite.
Releasing
- Align the version in
pyproject.tomlandfullmetalcopy/__init__.py. - Update
CHANGELOG.md. - Run
ruff format .,ruff check .,mypy fullmetalcopy, andpytest. - Build artifacts:
python -m build(optionallytwine check dist/*). - Tag:
git tag -s v0.2.0 -m "Release 0.2.0"thengit push origin v0.2.0. - Upload to PyPI (trusted publishing or
twine upload dist/*).
License
MIT — see LICENSE. Release history: CHANGELOG.md.
Metadata
Release files for fullmetalcopy 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| fullmetalcopy-0.2.0.tar.gz | 14.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fullmetalcopy-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 30.4 kB
Release files / fullmetalcopy-0.2.0.tar.gz
| Download URL | fullmetalcopy-0.2.0.tar.gz |
|---|---|
| Size | 14.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d07562489c5de615c2771d1dcf1b5bc205a291ff0527d51da026cb9b76381bcd
|
|
BLAKE2b-256 checksum How to use checksums |
a710497d11e5a271f5114103fbfee2f0957dcde93fde405fdf74e4ccf6e43f2f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.14
|
Release files / fullmetalcopy-0.2.0-py3-none-any.whl
| Download URL | fullmetalcopy-0.2.0-py3-none-any.whl |
|---|---|
| Size | 15.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a6bad390867233f146bcae14a5e876e43a9b091f095fdb50584afb0e16d95755
|
|
BLAKE2b-256 checksum How to use checksums |
001b66fe109ff8cd0c648e20514f57c951f986b12f77da65f5c3c12cfed38dd2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.14
|