fastanki

Python tools for Anki

Usage

Installation

Install latest from pypi

$ pip install fastanki

Documentation

from fastanki import *
import os
user = os.environ['ANKI_USER']
passw = os.environ['ANKI_PASS']

Functional API

add_card lets you create a new card with a single function call. Just pass your field values as keyword arguments. By default it uses the Basic note type and Default deck, but you can specify any model, deck, or tags you like.

notezh = add_card(Front='你好', Back='hello')

find_cards searches your collection and returns a list of Card objects. Pass any Anki search query as the first argument. Common query patterns: - deck:Spanish — cards in a specific deck - tag:vocab — cards with a tag - front:hello — match field content - is:due — cards due for review - added:7 — added in the last 7 days

Combine with spaces (AND) or OR: deck:Spanish tag:verb finds Spanish cards tagged “verb”.

cards = find_cards("deck:Default")
cards
[Card(1764738198390, nid=1764738198390, due=49, ivl=0, queue=0)]
cards[0]

Card 1764738198390 (nid: 1764738198390, due: 49, ivl: 0d, queue: 0)

find_card_ids("deck:Default")
[1764738198390]

find_notes searches your collection and returns a list of Note objects (rather than Card objects). The query language is the same as find_cards — all the same search patterns work. The difference is that find_notes returns one result per note, while find_cards may return multiple cards if a note generates more than one card (e.g., with Cloze or Basic-and-Reversed note types).

notes = find_notes("hello")
notes
[Note(1764738198390, Front='你好', Back='hello', tags=[])]
note = notes[0]
note

Front: 你好 | Back: hello

find_note_ids("hello")
[1764738198390]

update_note modifies an existing note’s fields and/or tags. Pass either a Note object or a note ID, along with any fields you want to change as keyword arguments. For tags: - tags=['a','b'] — replaces all tags - add_tags='newtag' — adds without removing existing tags

update_note(note, Back="updated answer", tags='testtag')

Front: 你好 | Back: updated answer | 🏷️ testtag

update_note(note, add_tags='moretagz')

Front: 你好 | Back: updated answer | 🏷️ testtag, moretagz

get_note(note.id)

Front: 你好 | Back: updated answer | 🏷️ moretagz, testtag

del_card([notezh, note])

✓ 1 change(s)

sync handles the entire sync lifecycle for you — opening the collection, authenticating with AnkiWeb, syncing, and closing up afterwards. The first time you sync, pass your AnkiWeb credentials; they’ll be saved for future use.

sync(user=user, passw=passw) # First time
# sync()  # after that
host_number: 5

Tool use

anki_tools()
&`[add_fb_card, find_notes, find_note_ids, find_cards, find_card_ids, get_note, del_card, update_fb_note, sync]`

Here are the available tools: &[add_fb_card, find_notes, find_note_ids, find_cards, find_card_ids, get_note, del_card, update_fb_note, sync].

Try to find all my notes. List the IDs and contents you see.

🤖Reply🤖

🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠

{
  "id": "toolu_016SJDA7Ls3sNefL5JdPpe7e",
  "call": {
    "function": "find_notes",
    "arguments": {
      "query": "*"
    }
  },
  "result": "[Note(1764724348705, Front='What is the capital of France?', Back='Paris', tags=[]),\n Note(176472434<TRUNCATED>"
}

Here’s what I found in your collection:

ID Front Back Tags
1764724348705 What is the capital of France? Paris (none)
1764724348737 What is the capital of France? Paris (none)

You have 2 notes, and interestingly they appear to be duplicates — both have the same question and answer about the capital of France. Would you like to remove one of them, or is there something else you’d like to do with your collection?

Delete them.

🤖Reply🤖

🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠

{
  "id": "toolu_011dKzVuBUvYyWEydkYtG1sF",
  "call": {
    "function": "del_card",
    "arguments": {
      "notes": "1764724348705"
    }
  },
  "result": "OpChangesWithCount(1)"
}
{
  "id": "toolu_016yZBTCXdNVpTydhTg8uTQZ",
  "call": {
    "function": "del_card",
    "arguments": {
      "notes": "1764724348737"
    }
  },
  "result": "OpChangesWithCount(1)"
}

Done! Both notes have been deleted (2 changes total). Your collection should now be empty. Would you like to add some new cards or do something else?

Try finding all notes again.

🤖Reply🤖

🧠🧠🧠🧠

{
  "id": "toolu_01JVQAUn4vRZZYxwJikGDAbn",
  "call": {
    "function": "find_notes",
    "arguments": {
      "query": "*"
    }
  },
  "result": "[]"
}

The collection is now empty — no notes found. Ready for a fresh start! What would you like to add?

Try adding a note of your choice using add_fb_card and tell me the id.

🤖Reply🤖

🧠🧠🧠🧠🧠🧠🧠🧠

{
  "id": "toolu_01EbRahwqio96vUq7AtMr2uU",
  "call": {
    "function": "add_fb_card",
    "arguments": {
      "front": "What is the speed of light in a vacuum?",
      "back": "Approximately 299,792,458 meters per second (about 3 \u00d7 10\u2078 m/s)"
    }
  },
  "result": "1764727897520"
}

Done! I added a physics flashcard about the speed of light. The note ID is 1764727897520.

Would you like to add more cards, or do something else with this one?

Try finding all notes again.

🤖Reply🤖

🧠🧠🧠🧠🧠

{
  "id": "toolu_01RpBYttNcGRr15YRP7HBhuy",
  "call": {
    "function": "find_notes",
    "arguments": {
      "query": "*"
    }
  },
  "result": "[Note(1764727897520, Front='What is the speed of light in a vacuum?', Back='Approximately 299,792,45<TRUNCATED>"
}

There’s now 1 note in your collection:

ID Front Back Tags
1764727897520 What is the speed of light in a vacuum? Approximately 299,792,458 meters per second (about 3 × 10⁸ m/s) (none)

What would you like to do next?

OK try get_note with it.

🤖Reply🤖

🧠🧠🧠🧠🧠🧠

{
  "id": "toolu_01Qy2LvuJSdxr421DrcdLq8y",
  "call": {
    "function": "get_note",
    "arguments": {
      "note_id": "1764727897520"
    }
  },
  "result": "Note(1764727897520, Front='What is the speed of light in a vacuum?', Back='Approximately 299,792,458<TRUNCATED>"
}

It works! The note comes back as a Note object with:

  • ID: 1764727897520
  • Front: What is the speed of light in a vacuum?
  • Back: Approximately 299,792,458 meters per second (about 3 × 10⁸ m/s)
  • Tags: [] (empty)

The serialization looks clean — the __repr__ method you defined earlier is being used. Anything else you’d like to test?

Delete it now.

🤖Reply🤖

🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠

{
  "id": "toolu_01HE56VzDm5pAydPYQS3pPRg",
  "call": {
    "function": "del_card",
    "arguments": {
      "notes": "1764727897520"
    }
  },
  "result": "OpChangesWithCount(1)"
}

Done! The note has been deleted. Your collection is empty again.

OK create, update, and verify a note now.

🤖Reply🤖

🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠

{
  "id": "toolu_018fiP5JTuM8nCVfW46os4Qu",
  "call": {
    "function": "add_fb_card",
    "arguments": {
      "front": "What is H\u2082O?",
      "back": "Water"
    }
  },
  "result": "1764731543071"
}
{
  "id": "toolu_0153ANMUNw3HQ5AdZzkxu6Mj",
  "call": {
    "function": "update_fb_note",
    "arguments": {
      "note_id": "1764731543071",
      "back": "Water (the chemical formula for water)",
      "add_tags": "chemistry"
    }
  },
  "result": "Note(1764731543071, Front='What is H\u2082O?', Back='Water (the chemical formula for water)', tags=['chem<TRUNCATED>"
}
{
  "id": "toolu_017TbPKpAPK6AJmSswhDDRhA",
  "call": {
    "function": "get_note",
    "arguments": {
      "note_id": "1764731543071"
    }
  },
  "result": "Note(1764731543071, Front='What is H\u2082O?', Back='Water (the chemical formula for water)', tags=['chem<TRUNCATED>"
}

All working! Here’s what happened:

  1. Added a card with Front=“What is H₂O?” and Back=“Water”
  2. Updated the back to “Water (the chemical formula for water)” and added the tag “chemistry”
  3. Verified with get_note — the changes persisted

The note ID is 1764731543071. What would you like to test next?

Try the various find ones that we haven’t done yet.

🤖Reply🤖

🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠

{
  "id": "toolu_016JBXpr9shwyawqdBqf7diZ",
  "call": {
    "function": "find_note_ids",
    "arguments": {
      "query": "*"
    }
  },
  "result": "[1764731543071]"
}
{
  "id": "toolu_019mDgnKk6ifAinw6ej84LmG",
  "call": {
    "function": "find_cards",
    "arguments": {
      "query": "*"
    }
  },
  "result": "[Card(1764731543071, nid=1764731543071, due=47, ivl=0, queue=0)]"
}
{
  "id": "toolu_01BBTUkQNTZfCgU2ehbFxDAU",
  "call": {
    "function": "find_card_ids",
    "arguments": {
      "query": "*"
    }
  },
  "result": "[1764731543071]"
}

All three work! Here’s the comparison:

Function Returns
find_note_ids("*") [1764731543071] — just the note ID
find_note_ids gives just IDs (lightweight)
find_cards("*") Card(1764731543071, nid=1764731543071, due=47, ivl=0, queue=0) — full Card object with scheduling info
find_card_ids("*") [1764731543071] — just the card ID

Interesting that in this case the note ID and card ID happen to be the same (1764731543071). That’s because with a Basic note type, there’s exactly one card per note.

That just leaves sync — want me to try that too?

Sure. Delete that note, then sync.

🤖Reply🤖

🧠🧠🧠🧠🧠🧠

{
  "id": "toolu_013cYoVSkSeF2iUxFoRsFqWb",
  "call": {
    "function": "del_card",
    "arguments": {
      "notes": "1764731543071"
    }
  },
  "result": "OpChangesWithCount(1)"
}
{
  "id": "toolu_01Gx7MqumvhrTBiq92jdHytr",
  "call": {
    "function": "sync",
    "arguments": {}
  },
  "result": "host_number: 5"
}

Done! Note deleted and synced to AnkiWeb. All tools are working. 🎉

OO API

data_path()
Path('/app/data/.local/share/Anki2')
profiles()
['User 1']
col = Collection.open()
col.profile_path
Path('/app/data/.local/share/Anki2/User 1')
# First sync requires credentials
col.sync(user=user, passw=passw)

# Subsequent syncs use saved auth
col.sync()
col.models
  • Basic
  • Basic (and reversed card)
  • Basic (optional reversed card)
  • Basic (type in the answer)
  • Cloze
  • Image Occlusion
col.sched.deck_due_tree().children[0]

Default: 2 new, 0 learn, 0 review

col.decks
  • Default
mdl = col.models['Basic']
mdl['id']
1764648511131
note = col.new_note(mdl)
note['Front'], note['Back'] = 'hola', 'hello'
note

Front: hola | Back: hello

note2 = col.add(Front='hola', Back='hello', tags=['spanish'])
col.add_deck('Spanish::Vocab')
changes {
  deck: true
  browser_table: true
  browser_sidebar: true
  study_queues: true
  mtime: true
}
id: 1764727764420
deck = col.decks['Spanish::Vocab']
deck.add(Front='adiós', Back='goodbye')
deck.due

Vocab: 1 new, 0 learn, 0 review

col.remove_notes([note2.id])

✓ 1 change(s)

col.decks.remove([col.decks.id_for_name('Spanish')])

✓ 1 change(s)

col.sync()
host_number: 5
col.close()