obelize
Verified codemods for breaking Python dependency upgrades. obelize rewrites the code it can prove
safe to change, tells you why it left the rest, and runs your tests before and after. It ships
migrations from google-generativeai to google-genai, openai 0.x to 1.x and PyPDF2 to pypdf,
and for another library it makes one from two of its releases.
A library that breaks its API is slow to leave by hand, and a coding agent edits fast but gives no proof. obelize sits between them:
- It rewrites a place only when it can resolve the call and prove the rewrite with a migration pack, a file of data: the bundled ones are reviewed and cite the library's own guide. Every other place stays as written and is listed with its reason.
- It changes nothing until you pass
--apply, refuses to write over uncommitted changes, andundoputs the files back. - It runs your tests before the change and after it. A run is verified when they passed before and
still pass after, and
upgradecompares them test by test. - It runs on your machine, with no account and no telemetry, and makes no network call unless you configure a model yourself or ask it to read a library's releases from the package index.
Example
examples/quickstart/ is a small script on both old libraries, which the two
packs migrate one after the other. obelize fix prints this diff for it:
diff --git a/app.py b/app.py
--- a/app.py
+++ b/app.py
@@ -3,22 +3,26 @@
import os
import sys
-import google.generativeai as genai
-import PyPDF2
+from google import genai
+from google.genai import types
+import pypdf
-genai.configure(api_key=os.environ["GEMINI_API_KEY"])
-model = genai.GenerativeModel("gemini-2.5-flash", generation_config={"temperature": 0.2})
+client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
def read(path):
if path.endswith(".pdf"):
- return "\n".join(page.extract_text() for page in PyPDF2.PdfReader(path).pages)
+ return "\n".join(page.extract_text() for page in pypdf.PdfReader(path).pages)
with open(path, encoding="utf-8") as handle:
return handle.read()
def summarise(text):
- response = model.generate_content(f"Summarise this in two sentences:\n\n{text}")
+ response = client.models.generate_content(
+ model="gemini-2.5-flash",
+ contents=f"Summarise this in two sentences:\n\n{text}",
+ config=types.GenerateContentConfig(temperature=0.2),
+ )
return response.text
diff --git a/requirements.txt b/requirements.txt
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,2 @@
-google-generativeai==0.8.6
-PyPDF2==3.0.1
+google-genai>=1.55
+pypdf>=6.19
The model object has no counterpart in google-genai, so its model name and generation config
move into the call, and each dependency line moves to its new package.
Install
Run it once, in your project's directory, with uv:
uvx obelize scan
Or keep it installed, with uv tool install obelize or pipx install --python python3.12 obelize.
obelize needs Python 3.12 or newer and runs on Linux, macOS and Windows; the project it migrates
needs only what the new library does. Getting started has the details.
Quickstart
In a git repository with everything committed:
uvx obelize scan
uvx obelize fix
scan lists each import, call and dependency line of the old libraries that it can resolve, and
whether it can migrate it. fix is a dry run until you pass --apply: it prints the plan and the
diff, and writes only its own record under .obelize/, which git ignores.
To apply the change and have your tests check it, first install the new libraries, here
google-genai and pypdf, into your project's environment beside the old ones, without changing
a tracked file. Your tests then run once before the change and once after:
uv pip install --python .venv/bin/python google-genai pypdf
uvx obelize fix --apply --verify ".venv/bin/python -m pytest -q"
git diff
The exit code says what is left, and most runs end with a Next: line naming the command to run:
| Exit code | Meaning |
|---|---|
0 |
Everything it found is migrated and your tests passed. |
4 |
Some places are left for you. REPORT.md in the run folder lists each with its reason. |
3 |
Your tests failed after the change. obelize undo puts the files back. |
5 |
obelize refused to apply over uncommitted changes, or to run a command it does not trust. |
6 |
The change is written, but your tests gave no verdict. |
After a run explains each code. For openai, 0.x and 1.x are one install,
so it takes four steps instead: see Getting started.
Upgrading a dependency
For a library with no pack, obelize makes one from the public API of two of its releases and tries the move in a copy of your repository:
uvx obelize outdated
uvx obelize upgrade pydantic
uvx obelize upgrade pydantic --apply --yes --branch --agent claude_code
outdated lists what has moved on. upgrade is a dry run: it plans the move and prints the diff.
With --apply --yes it installs the old and the new release into two environments made with uv,
runs your tests in each, and writes the move only if no test that passed before fails after;
--branch commits it to a new branch and leaves your working tree alone. --agent puts the places a
pack can only flag, and then the tests the move broke, to your own coding agent one at a time, with
every tool of the agent turned off, and obelize checks each answer before it reaches the copy. The
tests are the check: a weak suite proves little. Upgrading a dependency has the
steps and the four runs on three public repositories.
With a coding agent
An agent migrates more than obelize does, and obelize gives the migration a check. A study of 321
real Python library migrations found that language models got 89 to 94 percent of the individual
code changes right, but only 36 to 64 percent of whole migrations passed the tests that passed in
the developers' own migration (arXiv 2504.13272). Let obelize
write what it can prove, give the agent the places it lists, and run obelize scan and your tests
until nothing is left. With a coding agent has the steps, and
skills/obelize/SKILL.md is the same workflow as a skill file.
Commands
| Command | What it does |
|---|---|
obelize scan |
Lists the imports, calls and dependency lines a migration affects, or with --library every use of a library that has no pack, and edits nothing. |
obelize fix |
Plans the migration and prints its diff. --apply writes it, and --verify runs your tests before and after. |
obelize verify |
Runs the verification again for an applied run. |
obelize undo |
Puts back the files a run wrote, unless they were edited since. |
obelize mcp |
Serves obelize to a coding agent over the Model Context Protocol, with tools that take no command and no consent. |
obelize outdated |
Lists the dependencies a repository declares that the package index says have moved on, and what can migrate each. |
obelize upgrade |
Shows what moving one dependency to a newer release would change, with a bundled pack or one made from the two releases. --apply tries the move in a copy and runs your tests before and after. |
obelize pack validate |
Checks a migration pack against the pack format. |
obelize pack test |
Runs a migration pack over its own fixtures and sources. |
obelize pack new |
Makes a migration pack that loads, with fixtures and sources to edit. |
obelize pack synth |
Makes a migration pack from the public API of two releases of a library, read from the package index without installing or running anything. |
docs/CLI.md has every flag and exit code. The formats, from the flags to the run folder, may change in any 0.x release, and CHANGELOG.md lists each change.
Packs
| Pack | Rewrites | Leaves to you |
|---|---|---|
gemini/google-generativeai-to-google-genai |
The import, configure, the model object and its generation, chat and streaming calls, safety settings, file uploads and the dependency line. |
Every tool declaration, since automatic function calling is on by default in google-genai. |
openai/openai-0-to-1 |
Seven calls (ChatCompletion.create and six more) to the module client, the reads of their results, api_base, three exception classes and the dependency line. |
Async and streamed calls, Azure, other resources, and any call whose arguments or result reads it has not checked. |
py-pdf/pypdf2-to-pypdf |
The import, the names that exist unchanged on both sides, and the dependency line. | The names PyPDF2 3.0.0 removed and the ones pypdf dropped. |
Your own packs load from a directory your ~/.config/obelize/config.yml lists as pack_dirs. For a
library that has none, obelize pack synth makes one from the public API of two of its releases.
Packs says what each bundled pack covers, and the roadmap says
which migrations come next.
How it works
migration pack -> scan -> plan -> codemod -> patch -> verify -> report
(cites the guide) every what libcst only your what is left,
place it can rewrites proven tests and why
found prove edits twice
The codemods are libcst transformations, so a rewrite keeps the formatting and comments of the code around it. A rewrite that cannot be proven is refused, and the file stays as it was.
Safety
scan, and fix without --apply, change none of your files. obelize never pushes or merges, and
makes a branch and a commit only when you ask (upgrade --apply --branch), without touching your
working tree. In your repository it writes only its own .obelize/ folder and the files on its
plan, each through a temporary file and a rename, and it keeps what it reads from the package index
in your cache folder. upgrade --apply is the one command that installs packages: it asks
first and does it in a copy of the repository, with environments outside it. A verification command
runs only from your own configuration or a flag, or after you approve it at a prompt. Safety lists what
is mitigated and what is not, and the threat model and
privacy notes have the detail.
Benchmark
Every number comes from docs/BENCHMARK_RESULTS.md, which lists the cases behind it, and the method is in docs/BENCHMARK.md. All of them are for the Gemini pack: I measured the migration with obelize 0.1.0 and the scan again on a later scanner, with the same result. The other two packs have no accuracy figure; a dry run over 80 repositories each shows how many get a written plan.
| Measure | obelize | n |
|---|---|---|
| Scan precision | 100.0% | 63 usages reported in 5 repositories labelled by hand |
| Scan recall | 98.4% | 64 usages labelled by hand |
| Usages migrated | 25 (9.0%) | 277 usages in 20 public repositories |
| Repositories with a wrong edit | 0 | 20 |
| Repositories migrated and verified by their own tests | 0 | 20, of which 16 have no test command |
The second table puts obelize beside a general-purpose coding agent that was given Google's migration guide and one fixed prompt.
| Against a coding agent | obelize | The agent | n |
|---|---|---|---|
| Usages migrated | 17 (9.8%) | 154 (88.5%) | 174 usages in 13 of those repositories |
| Repositories with a wrong edit | 0 | 2 | 13 |
The two tools do different jobs. The agent migrated far more of these usages. obelize migrates the part it can prove, made no wrong edit where the agent made two, and lists the rest with a reason. The 13 repositories are the ones obelize's rules were written against, and the agent's column is a snapshot from 2026-09-22 that I have not measured again. I have not measured the two together.
Documentation
The documentation site has everything below, and each page is a file under docs/.
- Getting started, After a run, Upgrading a dependency and With a coding agent.
- CLI, Pack format, Run folder and Glossary.
- Limits, Known issues, Roadmap and Decisions.
- CHANGELOG.md, CONTRIBUTING.md, SECURITY.md and CODE_OF_CONDUCT.md.
Licensed under the Apache License 2.0.
Metadata
Release files for obelize 0.7.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 | Provenance | |
|---|---|---|---|---|
| obelize-0.7.0.tar.gz | 460.5 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| obelize-0.7.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 998.0 kB
Release files / obelize-0.7.0.tar.gz
| Download URL | obelize-0.7.0.tar.gz |
|---|---|
| Size | 460.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
64f34b2d6e5284dde6ce6521f741ecce8a939ad7f6edf1822db8afea9ba586ca
|
|
BLAKE2b-256 checksum How to use checksums |
b4dfeb008f2b73b4e57e39a82ce43f70ed1179c0d4fae98dee2b64dfb3e29a58
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.
Transparency logRelease files / obelize-0.7.0-py3-none-any.whl
| Download URL | obelize-0.7.0-py3-none-any.whl |
|---|---|
| Size | 537.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4f5b37ee935a055606ffb21427c42f7b19fb0911002344a24a9670ffaa8f046a
|
|
BLAKE2b-256 checksum How to use checksums |
34bbb2a16f8c5e3f743d8fe81bae354bd96922270218450ef01bb682ac273800
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.
Transparency logSecurity for obelize 0.7.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for obelize-0.7.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.