Migrating from genanki
genanki writes packages using an old collection format (schema v11). genanki2 writes the latest collection format (schema v18) and it can also read packages.
Migration from genanki to genanki2 depends on whether you have the package build script or not. If you do, simply translate it: see Translating a build script. It is recommended to use stable ids so that you can rerun the build script. See Identifiers that keep a deck updatable.
If you don't have a build script, but you have a shared package (.apkg file), try to use genanki2 to read it. If the package was written directly by genanki, the read will fail, telling you that genanki2 cannot read a v11 package. You will need to re-export a genanki-written deck using Anki. From then on, use a read-update-write flow with genanki2 to maintain your package.
Re-export a genanki-written deck
Import the package into Anki and export it again:
- In Anki, choose File > Export. Pick Anki Deck Package and your deck.
- Tick Include media.
- Untick Include scheduling information, so that your own review history does not ship with the deck.
- Untick Support older Anki versions.
- Read the export with
Package.read_apkg(path, keep_source_media=True), then change it and write a new version as described in Updating a package.
Reimporting the updated package does not create duplicates in your collection (apart as consequences of your updates) because the package contains all the identifiers that genanki wrote. See Identifiers that keep a deck updatable for details.
Finding notes
genanki2.guid_for() hashes differently from genanki.guid_for(), so if your update script uses this pattern, it will fail:
# The original build script:
import genanki
word = genanki.Model(1607392319, "Italian word", fields=[...], templates=[...])
casa = genanki.Note(
model=word, fields=["casa", "house"], guid=genanki.guid_for(word.model_id, "casa")
)
print(casa.guid) # MM8Z-D@is/
deck = genanki.Deck(2059400110, "Italian")
deck.add_note(casa)
genanki.Package(deck).write_to_file("italian.apkg")
# Import italian.apkg into Anki and export it again
# The genanki2 update script:
from genanki2 import Package, guid_for
pkg = Package.read_apkg("italian.apkg", keep_source_media=True)
word = pkg.collection.get_notetype(name="Italian word")
print(word.id) # 1607392319
print(guid_for(word.id, "casa")) # BMv|*`)wXK
print(pkg.collection.get_note(guid=guid_for(word.id, "casa"))) # None
Solutions:
-
Find by fields. Build a dictionary from fields that identify the note, as
examples/read_update_write.pydoes with{note["Italian"]: note for note in collection.notes}. -
Find by genanki's guid. If your script called
genanki.guid_for(*key)with stable values, compute the same guid and look it up withcollection.get_note(guid=...). Here is a recipe:import hashlib BASE91 = ( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" "!#$%&()*+,-./:;<=>?@[]^_`{|}~" ) def genanki_guid_for(*values: object) -> str: digest = hashlib.sha256("__".join(str(v) for v in values).encode()).digest() n = int.from_bytes(digest[:8], "big") out = "" while n > 0: n, r = divmod(n, len(BASE91)) out = BASE91[r] + out return outThen later you can use
pkg.collection.get_note(guid=genanki_guid_for(word.id, "casa")).
genanki's unstable default: A note built without guid= got genanki's default guid, a hash of all its field values, which changes when a field's value changes. Find those notes by a stable set of fields (solution 1).
Field and template ids
genanki writes fields and templates without ids, so Anki matches them by name, and renaming one makes the next import add a second notetype. Call Notetype.fill_missing_ids() on each notetype in the first version you ship with genanki2. The import gives the user's notetype those ids, and from then on a rename is matched by id. See Changing a notetype's fields or templates.
Translating a build script
The overall shape stays the same: notetypes, notes, a deck, a package.
| Step | genanki | genanki2 |
|---|---|---|
| Start | pkg, col = package() |
|
| Notetype | m = Model(1607392319, "Simple", fields=[...], templates=[...]) |
m = notetype("Simple", ["Q", "A"], [("Card 1", qfmt, afmt)], id=1607392319) |
| Note | n = Note(model=m, fields=["q", "a"]) |
n = note(m, ["q", "a"]) |
| Deck | d = Deck(2059400110, "Capitals") |
d = deck("Capitals") |
| Add | d.add_note(n) |
col.add_notes([n], d) |
| Write | Package(d).write_to_file("out.apkg") |
pkg.write_apkg("out.apkg") |
package() returns two objects. The package holds the media and is read from and written to disk; the collection holds the decks, notetypes and notes. Notes belong to the collection, and each card points at its deck.
col.add_notes() reads the whole collection on every call, so add all the notes for a deck in one call rather than one call per note.
Notetypes
| genanki | genanki2 |
|---|---|
Model(model_id, name, ...) |
notetype(name, ..., id=model_id) |
fields=[{"name": "Q"}] |
["Q"] |
fields=[{"name": "Q", "font": "Arial", "size": 20}] |
[field("Q", font_name="Arial", font_size=20)] |
templates=[{"name": n, "qfmt": q, "afmt": a}] |
[(n, q, a)] or [template(n, q, a)] |
css=..., latex_pre=..., latex_post=... |
the same keyword arguments on notetype() |
sort_field_index=1 |
sort_field="A" or sort_field_idx=1 |
model_type=Model.CLOZE |
cloze=True |
m.set_fields(...), m.set_templates(...) |
m.fields = [...], m.templates = [...] |
The other keys of a field or template dictionary become keyword arguments of field() and template():
| genanki key | genanki2 keyword |
|---|---|
font, size |
font_name, font_size |
rtl, sticky |
right_to_left, sticky |
bqfmt, bafmt |
q_format_browser, a_format_browser |
bfont, bsize |
browser_font_name, browser_font_size |
did |
target_deck_id |
media |
none, Anki no longer stores it |
- genanki also accepts
fieldsandtemplatesas a YAML string. genanki2 takes Python lists only. - Without
id,notetype()makes up a new id each run, and every rebuild then arrives as a new notetype. Pass a fixedid(see Identifiers that keep a deck updatable), or read the notetype from the previous package. -
genanki works out a note's cards by approximating Anki's rules and the approximation is wrong for some templates. genanki2 applies Anki's rules (see Card generation), so a note can get more or fewer cards than genanki wrote. genanki's errors:
Note genanki genanki2 and Anki front {{#A}}{{B}}{{/A}}{{C}}, onlyAfilledno card card 0, through the fallback front {{text:A}},Afilledno card card 0 a field holding only a space counted as filled counted as empty a cloze in a field the template does not pass through cloze:ignored gives a card {{c1,2::…}}card 0 cards 0 and 1 {{c1::{{c2::…}}}}card 0 cards 0 and 1 {{c600::…}}card 599 card 499 genanki writes a note without any card when its requirements fail, where Anki would give it card 0.
Notes
| genanki | genanki2 |
|---|---|
Note(model=m, fields=[...]) |
note(m, [...]) |
tags=["a", "b"] |
tags=["a", "b"] |
guid=... |
guid=... |
guid_for(*values) |
guid_for(notetype_id, *key), which gives other guids |
sort_field=... |
none, Anki computes it from the notetype's sort field |
due=5 |
card.due = 5 on the cards col.add_notes() returns |
n.cards[1].suspend = True |
cards[1].suspend(), on the cards col.add_notes() returns |
n.fields[0] |
n.fields[0], or n["Q"] by field name |
- Without
guid, genanki hashes all the field values, so editing a field turns the note into a new one on the user's side. genanki2 picks a random guid instead, which is not better for a rebuild: pass a guid (see Identifiers that keep a deck updatable), or read the note from the previous package. genanki2.guid_for()gives other guids thangenanki.guid_for()for the same values. For a deck people have already imported, keep genanki's guids: usegenanki_guid_for()from Finding notes and call it where the script calledgenanki.guid_for().- genanki gives every card position 0. genanki2 numbers new cards in the order that notes are added, which is the order they are studied in.
- A tag cannot contain a space. genanki2 also cleans each part of a tag (separated by
::) the way Anki does. -
col.add_notes()returns the cards grouped per note, in the order of the notes:for front, back in col.add_notes(notes, d): back.suspend() -
A suspension reaches the user only when they tick "Import any learning progress" while importing. Without it, which is the default, Anki imports every card as a new card and keeps only its position.
Decks
| genanki | genanki2 |
|---|---|
Deck(deck_id, name) |
deck(name) |
description="..." |
description="..." |
d.add_note(n) |
col.add_notes([n], d) |
d.add_model(m) |
not needed, the notes bring their notetype |
d.write_to_file(path) |
pkg.write_apkg(path) |
- Anki recognises a deck by its name, so genanki2 has no deck id to keep stable.
- An import creates only the decks and notetypes that the package's cards use. An empty deck is not imported by Anki.
Packages and media
| genanki | genanki2 |
|---|---|
Package([d1, d2]) |
one pkg, col = package(), with every deck in col |
Package(d, media_files=["a.mp3"]) |
pkg.media.append("a.mp3") |
p.write_to_file(path) |
pkg.write_apkg(path) |
- genanki packs media in no fixed order, and two paths with the same file name both get packed under that one name. genanki2 keeps the order you append in and refuses two paths with the same file name.
pkg.trim_unused_media()drops every media file that no note, template or stylesheet refers to.
Builtin notetypes
| genanki | genanki2 |
|---|---|
BASIC_MODEL |
BASIC_NOTETYPE |
BASIC_AND_REVERSED_CARD_MODEL |
BASIC_AND_REVERSED_NOTETYPE |
BASIC_OPTIONAL_REVERSED_CARD_MODEL |
BASIC_OPTIONAL_REVERSED_NOTETYPE |
BASIC_TYPE_IN_THE_ANSWER_MODEL |
BASIC_TYPE_IN_THE_ANSWER_NOTETYPE |
CLOZE_MODEL |
CLOZE_NOTETYPE |
IMAGE_OCCLUSION_NOTETYPE |
These have different ids from genanki's, so to Anki they are different notetypes. A deck that used BASIC_MODEL and is rebuilt with BASIC_NOTETYPE gives its users a second notetype and a second copy of every note. For a deck people have already imported, read the notetype from the export instead, as described in Re-export a genanki-written deck.
What genanki2 does not have
write_to_file(path, timestamp=...), which genanki uses to make ids and modification times repeatable between runs.write_to_collection_from_addon()andwrite_to_db(), which write into an open Anki collection from an add-on.