Anki scripting crimes, part 2: Managing Anki templates with python code

Welcome back to the second of a series of blog posts on advanced scripting in Anki, a flashcard app that you can use to remember vocabulary and facts.

In the first post Escape the text box and use npm dependencies on Anki cards we have established a convenient place for ourselves where we can develop code for cards outside of Anki in a testing project, with live reload, a way to test different values for Anki tags like {{Front}} in the browser without going back to Anki, and all the npm dependencies we need included directly on the cards. We also learned to, when the cards look good in the browser, build the code into a set of standalone HTML files (front.html and back.html usually), which we can then paste back into Anki to actually use our code for learning.

This already gives us a lot of options, but it's also a bit tedious, especially if you have a lot of different templates. Right now, every template needs a separate copy/paste action from us. For more advanced cards, where we have 10 cards for each note, each with its own template, this means 20 times copy/paste. Is this really necessary?

No, not if you use Anki's python package. In this second blog post of the series, we will learn to create Anki packages (APKGs) from python code, with our custom templates set in code.

As was the case last time, you can follow along on your computer, or view the final source code on University of Vienna Gitlab.

If you have read and followed along on the first post, you can continue with the same code. If you have not, consider checking out the solution of the last post and starting from there.

The anki python package

Instead of actually clicking and pasting, the state of an Anki collection can also be read and modified with Ankis comprehensive python package, which also has some official documentation.

The package lets you load an Anki collection, manage note types (with templates!), add decks and cards, and many other things. Basically, everything that you can click, you can also do with the python package, and more. In this post we will use it to import and export data in the form of APKGs, to modify templates, and to create entirely new note types from python code.

Options to manage templates with python code

To manage templates, we have three basic options:

Open heart surgery: turn off Anki and work directly on the collection you use to learn, with no import/export,

Reference APKG approach: export the note type you want to modify in an APKG with at least one card of the right type, create a new APKG with modified templates in code, and import that back into Anki,

Creating complete note types in code: work without a reference and create the note type entirely outside Anki.

We are not going to cover the open heart surgery approach. It takes the least clicking, but comes with significant drawbacks:

If your Anki application and the python package have different versions, you could damage your data in Anki,

you need to remember to keep the Anki application off the whole time while working on it in code,

a bug in your code could potentially remove data or make unintended destructive changes.

Instead we will first try the simpler approach of working with a reference APKG, and then proceed to creating an APKG without a reference.

Working with a reference APKG is especially useful if you have vocabulary in Anki, and just want to update a lot of templates for this vocabulary without too much clicking.

Creating an APKG without a reference is a bit more involved, but allows you to create entirely new note types from code and manage of all aspects of them. This is especially useful if you not only want to create templates from code, but also data like notes, or when applying advanced techniques were you need templates and data to match in some way.

Changing the templates

To be able to see if we successfully changed the templates in the APKGs we generate, we first need some changes that we can apply. For example, you can an additional horizontal line with , or you can get creative. You can use parcel to test your ideas in your browser:

npm install # if you havn't already (this installs parcel to node_modules)
node_modules/.bin/parcel serve --config=./parcel-dev.json front.html back.html

Then, with your changes to the templates in place, generate new dist/front.html and dist/back.html for the finished HTML of back and front side, respectively:

node_modules/.bin/parcel build front.html back.html

Setting up your PC for python development with Anki

Before we can start coding, we need something to work with: a reference APKG with a note type to try the approach of modifying a reference, and, critically, somewhere to put our python code, with the anki package set up so we can use it. Let's get started.

Creating a reference APKG

One of the approaches we are going to try requires a reference file with a card of the note type we want to modify.

Let's use the Japanese vocabulary note type from the last post. To export a note type, we also need to export a card, so let's create an APKG in Anki from the eel card.

Make sure the card is in its own deck (otherwise just create a new card in a new deck), then in Anki's main window click the cogwheel and select "Export…". Use the default format of "Anki Deck Package" and select support for older Anki versions. The rest can be left at default values:

Exporting the reference APKG that contains the note type we want to modify

Save your APKG as reference.apkg to your code directory (or a clone of the solution of the last post).

Setting up python and pipenv

First off, you are going to need python installed on your computer. There are many helpful resources online to help you do this, for example the Beginners Guide on the official python wiki. When you're done, you should be able to run python and pip on your command line.

Then, we need something to manage the anki package for us. This package changes from time to time and may require that you change your code, so it's helpful for stability if you work with a specific version of the anki package. Ideally, you wouldn't choose a version at the system level, but make a selection for each Anki code project you are working on. This is easy to do with virtual environments in python, which are a way of running python code with a specific version of packages that we need (just anki in our case, but you may need more in the future).

There are many ways to manage virtual environments, and in this post we are going to use a tool called pipenv. The idea is to have a file called the Pipfile which (similar to package.json for JS) specifies the packages and versions we need. Please refer to the pipenv documentation to install the tool (for example with pip install pipenv --user and then adding the user site packages to your $PATH environment variable). After setup, you should be able to run pipenv on your command line.

With everything set up on our computers, we can now proceed to creating a venv with anki installed.

Creating a python venv with the anki package installed

Let's create a file Pipfile in the directory with our code that asks to install a specific version of the anki package:

[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
anki = "==25.2"

Then, to create the venv from that file:

cd your-code-with-pipfile
pipenv install

Cool! Now if we want to run a python script wabbalabbadubdub.py with the anki version we asked for, we can run pipenv run python wabbalabbadubdub.py. You can also select the created venv in many IDEs like Visual Studio Code (ctrl+shift+p: select interpreter) to get auto-completion and inline documentation for the package.

And that's it with setup, we can now start coding.

Creating an APKG with modified templates from a reference APKG

Let's start with the simpler approach of loading the reference.apkg we created previously and using python code to create a new one with modified templates that can be imported back into Anki. Let's create a new file create_from_reference.py to do that.

First, we need a collection to work with. Instead of using an existing collection, we are going to create a new one with the standard file ending of .anki2. If one already exists from a previous run of our script, we delete it first:

from anki.collection import *
import os

try:
# delete if exists already
os.remove("temporary.anki2")
except OSError:
pass
# then start fresh
col = Collection("temporary.anki2")

The new collection is like a fresh install or a new profile in Anki. It only has a few basic note types and an empty "Default" deck set up, but not the "Japanese vocabulary" that we want to change. To import the note type, we ask Anki to import reference.apkg into the new collection for us:

from anki.import_export_pb2 import *

col.import_anki_package(ImportAnkiPackageRequest(
# this is what you exported from Anki
package_path="reference.apkg",
# the options don't matter that much in our case
# since the collection we import into is empty anyway
options=ImportAnkiPackageOptions(
merge_notetypes=True,
update_notes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_ALWAYS,
update_notetypes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_NEVER,
with_scheduling=False,
with_deck_configs=False,
)
))

Next step is to find the ID of the note type we want to change. Let's use something like this to set note_type_id_for_template_replacement with that ID:

note_type_id_for_template_replacement = None
for note_type in col.models.all_names_and_ids():
if note_type.name == "Japanese vocabulary":
note_type_id_for_template_replacement = note_type.id
if note_type_id_for_template_replacement is None:
raise Exception("Note type to modify templates of not found")

With the ID in hand, we can get a nested dictionary that fully describes the note type, including templates. In python code, note types are called "models":

note_type = col.models.get(note_type_id_for_template_replacement)

The top-level dictionary has an entry under the key "tmpls" with all the templates for the note type stored as dictionaries in a list. Usually, every template will create a card using the information of the note type. In our case there is just one entry because we only quiz the Kanji given "Front". Let's change the templates using the HTML we have built before by setting the "qfmt" and "afmt" fields of the nested dictionary to the HTML of front and back side, respectively:

with open("dist/front.html", "r") as front_file:
with open("dist/back.html") as back_file:
front_html = front_file.read()
back_html = back_file.read()
note_type["tmpls"][0]["qfmt"] = front_html
note_type["tmpls"][0]["afmt"] = back_html

With our new modifications to note_type in place, the note type (or "model") is ready to be saved back into the collection with:

col.models.update_dict(note_type)

Cool, now the temporary collection contains the notes from our reference, but with new templates. All that's left is to write that to a file that can be imported to your (or anyone else's) Anki:

col.export_anki_package(
# anki fails if this is just an apkg filename without a dir,
# with ./ it's fine
out_path="./cool_new_template.apkg",
options=ExportAnkiPackageOptions(
with_deck_configs=False,
with_media=True,
with_scheduling=False,
legacy=True,
),
limit=None)

Let's run to see if it worked:

pipenv run python create_from_reference.py

You should now see a new APKG in your code directory, named cool_new_template.apkg. It should contain the new template, but did it work? Let's find out. Open up Anki again and select "File | Import…" in the menu bar, selecting cool_new_template.apkg in the file dialog. In the import settings make sure to enable the setting to merge note types:

Importing the APKG created by our script and allowing it to change the note type in Anki

Click on the deck to start a practice session and it should now include your updated template:

The templates now include the dashes without manually changing the note type in Anki

It worked! And you can naturally expand this approach to any number of templates. So if your notes should create 10 cards, you now have a way to update all 10 templates in one go from a script. You are not limited to just working with note types either. You can also create vocabulary in code, and include that in your APKG. We'll explore creating vocabulary in more detail in a future post.

Create an APKG with custom templates from scratch

Reading the last two sections, you may have asked yourself: do I really need an exported APKG for the note type at all, can't I just create the note type from scratch in code, add data from some source like a CSV, export an APKG, with no input APKG from Anki needed?

Well, yes, it's actually fairly easy to add a note type from python, but the catch is that when you create a note type, no matter how you choose to name it, Anki will pick a unique ID for the note type based on (or rather exactly equal to) the current time when you created it, so what you do in your script is not reproducible. If you run the script again, and import into Anki, the notes will be treated as completely separate from the ones you already have from a last import, because their note type has a different ID.

There is no obvious way built into the anki package to pick a predictable ID manually, so that importing again later would update the templates from before. If your script creates note types from scratch and you run your script again at a later point in time and create a new APKG and import it into an Anki that had the old version, then the note type from your last import and the new note type from this import are different by ID, and consequently the notes from the last import will not be updated with the new template and any notes you created manually in Anki and not from the script will still have the old note type. Working around this is possible in Anki by merging the note types manually every time you do an import, but that's a bit tedious. So do we just have to live with having an APKG that's just there for an ID where we will change most of the templates anyway? Actually, no. There is an offical API to pick a custom ID and I already told you what it is. Sort of official. If you squint a little.

It's the time of creation, and that alone that determines the note type ID. Consequently, all you have to do is to change your computer's clock to some time of your choosing, and to then keep it in place while your script is doing its work of creating a note type. If you do that, the ID is perfectly predictable and if you later import the APKG your script produces into your normal Anki, then this Anki will acknowledge that the note type it just imported and the one it already has from a previous run of the script, are, in fact, the same thing, despite actually having been created twice at two different points in time.

An easy way to fake a time for your script is to use something like libfaketime which works by intercepting library calls and instead of querying the time from the operating system, giving a fake value. As far as I know, this only works on Linux and Mac OS (or similar operating systems), but there probably are ways to achieve something like this on Windows and other operating systems too. I'll describe the approach using libfaketime, and you can follow along if you use an operating system that supports it (or with a VM), or if you have found a tool that does something similar for your OS.

To add the python package to our venv, we are going to add it to our Pipfile, by adding this line after the anki dependency:

libfaketime = "==3.0.0"

Then you can install the dependencies again from your shell:

pipenv install

Let's start a new script create_from_scratch.py together that begins with some boilerplate code that will allow us to change the time later.

from libfaketime import fake_time, reexec_if_needed

reexec_if_needed() # setup for faking the time later, which may require a restart

Then, we create a temporary collection again, same as before:

from anki.collection import *

try:
os.remove("temporary.anki2")
except OSError:
pass
col = Collection("temporary.anki2")

This time we don't import anything, but add dictionaries to col.models that we create from scratch.

A complete note type is a rather big dict, and I'll be honest with you, I really only understand the field and template parts. A little. For the rest it worked well for me to stick to default values (that I don't completely understand).

Let's start with the fields, stored in flds of the note type dictionary. Each entry has a name, like Front for {{Front}} in the template, and an ord, which is the sorting order of the field. The field with ord 0 is special because Anki requires that it is non-empty. Then there are a couple values that look styling-related, though I'm not sure what they do exactly. You don't really need them, since you can use CSS. A complete field looks like this:

{
'name': "Front",
'ord': 0, # Field No.1, this one must always be non-empty
'id': 0, # must be unique
# not sure what the rest does, just use CSS for styling
'sticky': False,
'rtl': False,
'font': 'Arial',
'size': 20,
'description': '',
'plainText': False,
'collapsed': False,
'excludeFromSearch': False,
'tag': None,
'preventDeletion': False
}

We already know qfmt and afmt of the templates in tmpls. Templates additionally have an ord, analogous to the fields. Again, the extra fields look styling-related and a complete template would look something like this:

{
'name': 'English to Japanese',
'ord': 0,
'qfmt': front_html,
'afmt': back_html,
'bqfmt': '',
'bafmt': '',
'did': None, # this could maybe be the deck override option
'bfont': '',
'bsize': 0,
}

Combining fields and templates with a few more mystery fields, we can write down a full note type with a single template for translating from English to Japanese:

note_type = {
'id': 0, # the 0 has no effect, the actual ID will be the timestamp at the time of adding the note type
'name': "My cool note type",
'type': 0,
'mod': 0,
'usn': 0,
'sortf': 0,
'did': None,
'tmpls': [
{
'name': 'English to Japanese',
'ord': 0,
'qfmt': front_html,
'afmt': back_html,
'bqfmt': '',
'bafmt': '',
# this could maybe be the deck override option
'did': None,
'bfont': '',
'bsize': 0,
}
],
'flds': [
# Example
{
'name': "Front",
'ord': 0, # Field No.1, this one must always be non-empty
# not sure what this stuff does, just use CSS for styling
'id': 0, # a unique ID, this can be the ord
'sticky': False,
'rtl': False,
'font': 'Arial',
'size': 20,
'description': '',
'plainText': False,
'collapsed': False,
'excludeFromSearch': False,
'tag': None,
'preventDeletion': False
},
{
'name': "Back",
'ord': 1, # Field No.2, can be empty
'id': 1,
'sticky': False,
'rtl': False,
'font': 'Arial',
'size': 20,
'description': '',
'plainText': False,
'collapsed': False,
'excludeFromSearch': False,
'tag': None,
'preventDeletion': False
}
# … more fields like Rōmaji if you'd like them created
],
'css': '', # instead of putting it into the HTML you could also set CSS here
# note sure what the rest does
'latexPre':'\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage[utf8]{inputenc}\n\\usepackage{amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\n',
'latexPost':'\\end{document}',
'latexSvg': False,
'req': [
[
0,
'any',
[ 0 ]
]
],
'originalStockKind': 1
}

With the new note type dictionary stored in note_type, we can add it to our collection and give it a predictable ID by re-setting the time for a block of python code where we add it to the temporary Anki collection:

with fake_time("2025-04-07 18:45:00"):
# add the note in the past (or the future)
note_type_id = col.models.add_dict(example_note_type).id

We did it: a note type from scratch, with a stable ID. Now all that's left would be to export an APKG, but we don't have any cards yet. Let's add one using the new note type just for the sake of exporting:

# Step 4: add a card and deck to have something to export
note = Note(col, note_type_id)
# you need some sort of unique ID for Anki to know on the next import if you
# already have the note or not
note.guid = "test1"
# set values from the CSV, using the name of the field you chose in Anki
note["English"] = "tree"
note["Japanese"] = "木"
deck_id = col.decks.add_normal_deck_with_name("Test deck")
col.add_note(note, deck_id) # save the note to the collection

In the same manner, you could also also create lots of notes that you read or generate with python code. Just make sure that the guid is stable.

And then, same as before, we write an APKG that we can import back into Anki:

col.export_anki_package(
# anki fails if this is just an apkg filename without a dir,
# with ./ it's fine
out_path="./cool_new_template_from_scratch.apkg",
options=ExportAnkiPackageOptions(
with_deck_configs=False,
with_media=True,
with_scheduling=False,
legacy=True,
),
limit=None)

Let's run our script in the shell:

pipenv run python create_from_scratch.py

A new file cool_new_template_from_scratch.apkg will appear. Let's import it into Anki using the same process as above and then try the Test deck. Cool, it works!

A card using a note type that we created from scratch in python, without a reference note type

If you run the script again and re-import, you'll see that no new note type will be created, but the previous one will be updated. With this approach, you can manage all aspects of your note type in a git repository without any binary files.

Conclusion

Ok, I'll admit that was quite a bit of work but think about the possibilities.

Either of these approaches allows you to manage any number of templates or fields from code. The ability to create APKGs from code allows you to test these in a new Anki profile or installation before you let them loose on the Anki you use day-to-day, and when they are fine, share with your friends. By using code to update templates, especially when working with a lot of them, you also avoid inevitable copy/paste mistakes.

In this post we focused on updating templates and only scratched the surface of managing notes, decks and cards from python code. We will explore these topics in a lot more detail in the next post, where our main focus will be to use python code to create a lot of cards. We will also have a look at calculating values in notes automatically, when we add Hiragana using python code, instead of looking this up manually.

With a way to manage templates (from this post) and a way to manage vocabulary (in the next), you'll be able to make some truly amazing decks in a reproducible manner that you could easily integrate into a continuous integration (CI) system to create and distribute your Anki packages.

If you had any trouble following along, consider checking the solution for this post on University of Vienna Gitlab.

I hope you enjoyed this second post of my series on Anki scripting crimes and that you will be back for more next week, when we will create a whole lot of cards.