AniList API Wrapper
Complete, type-safe Python client for the AniList GraphQL API.
v1.0.0 Python 3.10+ No API Key MIT
Introduction
The AniList Wrapper provides a clean, type-safe interface to the AniList GraphQL API. It handles rate limiting, pagination, OAuth2, and serialization — no API key needed for public data.
Zero Config
No API key. Import and go for all public anime/manga data.
Type-Safe
Pydantic v2 models for all 55+ GraphQL types with full alias support.
Rate Limited
Token bucket at ~80 req/min stays safely under AniList's ~90 cap.
Rich Data
Media, characters, staff, studios, airing schedules, trends, user lists.
Quick Start
from anilist import AniListClient, MediaSeason
client = AniListClient()
# Get by ID
anime = client.get_media(1)
print(anime.title.romaji) # Cowboy Bebop
print(anime.average_score) # 86
# Search
results = client.search_media("Attack on Titan")
for m in results.nodes:
print(f"{m.title.english}: {m.average_score}%")
# Seasonal
summer = client.get_seasonal(2026, MediaSeason.SUMMER)
for m in summer.nodes:
print(m.title.romaji)
# Characters
spike = client.get_character(1)
print(spike.name.full) # Spike Spiegel
client.close()
from anilist import AniListClient
with AniListClient() as client:
anime = client.get_media(1)
trending = client.get_trending(per_page=10)
characters = client.search_characters("Mikasa")
Installation
pip install anilist-wrapper
Or from source: git clone ... && pip install -e .
Dependencies: httpx >= 0.25.0, pydantic >= 2.0.0
API Methods 18 methods
Get a single media entry by AniList ID.
anime = client.get_media(1) manga = client.get_media(30002, media_type="MANGA")
Search by title. Supports type, format, status, genre filters and all sort orders.
results = client.search_media("Cowboy Bebop", per_page=5, sort=MediaSort.POPULARITY_DESC)
for m in results.nodes:
print(m.title.english, m.average_score)
print(results.page_info.total) # total results
Known Issue AniList search endpoint occasionally returns HTTP 500 during high load.
Get media for a specific season.
from anilist import MediaSeason summer = client.get_seasonal(2026, MediaSeason.SUMMER, per_page=20, sort=MediaSort.POPULARITY_DESC)
Get currently trending anime or manga.
trending = client.get_trending(per_page=10) trending_manga = client.get_trending(media_type="MANGA", per_page=5)
Get a character by ID with name, image, description, media appearances.
spike = client.get_character(1)
Search characters by name with pagination.
lelouch = client.search_characters("Lelouch", per_page=5)Get a staff member by ID.
director = client.get_staff(97091)
Search for a studio by name.
wit = client.search_studios("Wit Studio")Get characters for a media entry. Filter by role: "MAIN", "SUPPORTING", "BACKGROUND".
chars = client.get_media_characters(1, per_page=10, role="MAIN")
Get staff (directors, animators, voice actors) for a media entry.
staff = client.get_media_staff(1, per_page=5)
Get production studios for a media entry.
studios = client.get_media_studios(1) for s in studios.nodes: print(s.name)
Get recommendations based on a media entry.
recs = client.get_media_recommendations(16498, per_page=5)
for r in recs.get("nodes", []):
mr = r["mediaRecommendation"]
print(mr["title"]["romaji"], mr["averageScore"])Get sequels, prequels, adaptations, and other related media.
Get daily trend stats (trending, popularity, score over time).
trends = client.get_media_trends(1, per_page=30)
Get all rankings (#1 in Action, #3 in 1999, etc.) for a media.
Get upcoming episodes with Unix timestamp filtering.
schedule = client.get_airing_schedule(per_page=10, not_yet_aired=True)
Known Issue Returns HTTP 400 in some configurations. Fix pending.
Get a user profile. Without args returns the authenticated viewer (requires OAuth).
user = client.get_user(name="SomeUser") me = client.get_user() # needs auth
Auth Required for viewer endpoint.
Get a user's anime or manga list. Requires OAuth for private lists.
alist = client.get_user_list(12345, media_type="ANIME", status=MediaListStatus.COMPLETED)
Get all possible media genres.
genres = client.get_genres() # ['Action', 'Adventure', 'Comedy', 'Drama', ...]
Get all media tags. status: 0=rejected, 1=pending, 2=accepted.
Get AniList site-wide anime/manga/user counts.
Known Issue Returns HTTP 400. Fix pending.
Enums
| Enum | Values |
|---|---|
MediaFormat | TV, TV_SHORT, MOVIE, SPECIAL, OVA, ONA, MUSIC, MANGA, NOVEL, ONE_SHOT |
MediaStatus | FINISHED, RELEASING, NOT_YET_RELEASED, CANCELLED, HIATUS |
MediaSeason | WINTER, SPRING, SUMMER, FALL |
MediaSort | ID, TITLE_ROMAJI, SCORE, POPULARITY, TRENDING, EPISODES, SEARCH_MATCH, FAVOURITES... (all with _DESC variants) |
MediaSource | ORIGINAL, MANGA, LIGHT_NOVEL, VISUAL_NOVEL, VIDEO_GAME, ANIME, WEB_NOVEL, LIVE_ACTION... |
MediaListStatus | CURRENT, PLANNING, COMPLETED, DROPPED, PAUSED, REPEATING |
CharacterSort | ID, ROLE, SEARCH_MATCH, FAVOURITES, RELEVANCE (+ _DESC) |
StaffSort | ID, ROLE, LANGUAGE, SEARCH_MATCH, FAVOURITES, RELEVANCE (+ _DESC) |
UserTitleLanguage | ROMAJI, ENGLISH, NATIVE, ROMAJI_STYLISED, ENGLISH_STYLISED, NATIVE_STYLISED |
ScoreFormat | POINT_100, POINT_10_DECIMAL, POINT_10, POINT_5, POINT_3 |
Models
All models are Pydantic v2 classes with populate_by_name=True — GraphQL camelCase fields auto-map to Python snake_case.
| Model | Key Fields |
|---|---|
Media | id, title, format, status, episodes, chapters, average_score, popularity, trending, genres, cover_image, next_airing_episode, rankings... |
MediaConnection | page_info: PageInfo, nodes: list[Media] |
PageInfo | total, per_page, current_page, last_page, has_next_page |
Character | id, name: CharacterName, image, description, gender, age, favourites, media |
Staff | id, name: StaffName, image, primary_occupations, years_active, favourites |
Studio | id, name, is_animation_studio, favourites, media |
User | id, name, avatar, statistics, options, media_list_options, donator_tier |
MediaList | id, status, score, progress, notes, media |
MediaTrend | media_id, date, trending, average_score, popularity, in_progress, episode |
AiringSchedule | id, airing_at (Unix), time_until_airing, episode, media_id |
FuzzyDate | year, month, day (all Optional[int]) |
Authentication
Public data requires zero auth. OAuth2 is only needed for user-specific operations (reading private lists, updating progress).
from anilist.auth import AniListAuth
from anilist import AniListClient
auth = AniListAuth(
client_id="YOUR_CLIENT_ID",
client_secret="YOUR_CLIENT_SECRET",
)
# Step 1: User visits this URL
print(auth.get_authorization_url())
# Step 2: Exchange the code from the callback
token = auth.exchange_code("code_from_callback_url")
# Step 3: Use with client
client = AniListClient(auth=auth)
me = client.get_user() # Returns the authenticated viewer
print(me.name)
# Save token to disk
auth.save("token.json")
# Later: restore
auth = AniListAuth.load("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET", "token.json")
client = AniListClient(auth=auth)
# Or load manually
auth.load_token(
access_token="...",
refresh_token="...",
expires_in=31536000, # ~1 year
)
CLI Usage
# Search anilist search "Attack on Titan" anilist search "isekai" --format TV --sort POPULARITY_DESC # Get by ID anilist get 1 anilist get 1 --json | jq .title.romaji # Seasonal anilist seasonal --year 2026 --season SUMMER anilist seasonal --year 2026 --season WINTER --per-page 5 # Trending anilist trending anilist trending --type MANGA # Characters & Studios anilist character "Spike Spiegel" anilist studio "Wit Studio" # Genres & Schedule anilist genres anilist schedule # User (requires OAuth) anilist user --name "someuser" --token YOUR_OAUTH_TOKEN
Rate Limiting
AniList allows ~90 requests per minute. The wrapper uses a token bucket algorithm defaulting to 80 req/min with burst support (up to 10 instantaneous).
# Stricter limit client = AniListClient(rate_limit_rpm=60) # More burst tolerance from anilist.rate_limiter import RateLimiter limiter = RateLimiter(rate=80, burst=20)
Exceptions
| Exception | When |
|---|---|
AniListError | Base class for all errors |
GraphQLError | API returns errors array in response |
RateLimitError | HTTP 429 — exceeded 90 req/min |
AuthenticationError | OAuth failure |
NotFoundError | Resource not found |
ValidationError | Input validation failure |
Limitations & Known Issues
| Issue | Status | Details |
|---|---|---|
search_media() 500 errors |
AniList Server | AniList search endpoint occasionally returns HTTP 500 under load. This is upstream — retry after a few seconds. |
get_airing_schedule() |
Fix Pending | Returns 400 in some configurations. Query structure needs adjustment. |
get_site_statistics() |
Fix Pending | Returns 400. GraphQL query needs schema correction. |
| No async support | Planned | Currently synchronous only (httpx). Async client planned for v1.1. |
| Mutations not implemented | Planned | Read-only operations only. SaveMediaListEntry, ToggleFavourite, etc. not yet wrapped. |
| Cross-module model refs | Resolved | Connection-type fields (characters, staff, studios, recommendations) on Media use object type. Fully typed connections available via dedicated getter methods. |
| Rate limit | 90 req/min | Hard cap imposed by AniList. The wrapper stays at ~80 with burst tolerance. |
| No GraphQL query customization | Planned | Methods use fixed queries. Custom query builder planned for advanced use cases. |
Architecture Constraints
- Single GraphQL endpoint:
POST https://graphql.anilist.co - No webhook/callback support from AniList
- No bulk mutation operations
- Adult content filtering controlled client-side via
is_adultfield - Fuzzy dates: month/day may be null for incomplete release data
Version 1.0.0 · MIT License · Built for Python 3.10+