github teng-lin/notebooklm-py v0.7.0

13 hours ago

[0.7.0] - 2026-06-04

Highlights

  • v0.8.0 error-contract runway. This release lands the additive half of a
    cross-SDK convergence on "absence and refusal raise; only success and
    async-lifecycle state are returned." You can adopt the forward-compatible form
    today and run on both 0.7.0 and 0.8.0 with no flag day:
    • Test your code against 0.8.0 today — set NOTEBOOKLM_FUTURE_ERRORS=1 to
      opt your process into the v0.8.0 error contract (get() raises
      *NotFoundError on a miss, all dict-style access on the typed returns
      ([...], .get(), in, .keys(), …) raises, and
      the deprecated wait_for_completion(interval=...) alias raises) without
      changing default behavior
      . Run your test suite with it on to find breakage
      before you upgrade. This is the "test-before-you-migrate" mechanism paired
      with the Upgrading to v0.8.0 guide.
    • get_or_none() — a new, silent optional lookup on
      sources / artifacts / notes / mind_maps that returns the object or
      None and never warns. It is the sanctioned replacement for the now-soft
      get()-returns-None pattern.
    • get() now warns on a miss (still returns None this release) and will
      raise the typed *NotFoundError for its domain in v0.8.0 (#1247).
    • Typed *NotFoundError per domainNoteNotFoundError /
      MindMapNotFoundError join the existing source / artifact / notebook errors,
      all catchable via the NotFoundError umbrella.
  • Breaking: rename() returns the renamed object; delete() returns None.
    rename() now re-fetches and returns the live object (raising *NotFoundError
    on a missing target), and delete() returns None and is idempotent on an
    already-absent target. See Breaking changes below before upgrading.
  • Typed dataclass returns for research.poll / start /
    wait_for_completion, artifacts.generate_mind_map, and sources.get_guide
    (ResearchStatus, ResearchTask, ResearchSource, ResearchStart,
    MindMapResult, SourceGuide) — attribute access instead of untyped dicts,
    with a backward-compatible read-only mapping bridge.
  • Unified client.mind_maps surface over both backends (note-backed +
    interactive), plus client.artifacts.retry_failed() to retry a failed
    Studio artifact in place (and a matching notebooklm artifact retry command).

Breaking changes

⚠ BREAKING — rename() returns the renamed object; delete() returns None.

These return-type changes ship now, as a clean break with no deprecation
runway
, because the old returns were never usable contracts a caller could
depend on in good faith. (Contrast get()'s None-on-miss, which is a
real, documented contract and keeps its full deprecation runway to v0.8.0 —
see issue #1247. The coherent story: reads/renames are missing-strict;
deletes are absence-idempotent.
)

rename() → returns the renamed object, raises *NotFoundError on a
missing target
(issues #1255, #1256):

  • artifacts.rename previously returned None even on success (an
    unusable return); it now re-fetches and returns the renamed Artifact,
    raising ArtifactNotFoundError when the target is absent.
  • sources.rename previously fabricated an unverified
    Source(id, title) when the RPC echoed nothing (a silent-false-success
    bug); it now prefers the UPDATE_SOURCE echo, falls back to an internal
    fetch, returns the real Source, and raises SourceNotFoundError when the
    target is absent. The fabrication is gone.
  • notebooks.rename already returned the re-fetched Notebook (the
    reference behavior) — unchanged.
  • mind_maps.rename (both note-backed and interactive backings) now returns
    the renamed MindMap and raises on a missing target.
  • Error taxonomy: only genuine absence (empty-payload / absent-from-list,
    detected via a content/list lookup — not a transport 404) maps to a
    *NotFoundError. Transport / 429 / 5xx / auth errors propagate as
    themselves
    and are never laundered into a synthetic *NotFoundError.
  • Bulk opt-out: every rename() accepts return_object: bool = True.
    Pass return_object=False to skip the hydrate re-fetch and return None
    (artifacts' re-fetch is a full LIST_ARTIFACTS, so bulk renamers that
    ignore the return should opt out to avoid N extra list calls).

delete() → returns None, idempotent on a missing target (issue #1211):

  • notebooks / sources / artifacts / notes.delete and
    notes.delete_mind_map (and mind_maps.delete) previously returned a
    hardcoded True; they now return None. The old True was a tautology
    (never False), but True → None is a real, observable flip from truthy
    to falsy:
    # BEFORE (entered the block; delete always returned True)
    if await client.sources.delete(nb_id, src_id):
        ...  # this branch no longer runs — delete() now returns None (falsy)
    Drop the if; call delete() for its effect. Use get() first if you
    need to assert existence.
  • Idempotent: deleting an already-absent target succeeds (returns
    None); it does not raise *NotFoundError. This matches HTTP DELETE
    idempotency and keeps retry/teardown loops clean. (The one exception is
    mind_maps.delete without an explicit kind, which must list to pick
    the right RPC family and so raises ValueError for an unknown id; pass
    kind= to delete idempotently.)
  • Real failures still raise: allow_null=True tolerates only a null
    result, not an RPC/HTTP error — a 403 / 5xx / auth / transport
    failure on delete still propagates. "Idempotent on missing" is not "swallow
    all errors."

⚠ BREAKING — lapsed v0.6.0-targeted deprecations removed.

These deprecation shims advertised removal in v0.6.0, which has shipped, so
they have now been removed. This is a pre-1.0 breaking change. See
docs/deprecations.md "Removed in v0.7.0".

  • Positional wait / wait_timeout on SourcesAPI.add_url / add_text /
    add_file / add_drive
    — these parameters are now keyword-only.
    Passing them positionally raises TypeError.
    # BEFORE (deprecated, emitted DeprecationWarning)
    await client.sources.add_url(nb_id, url, True, 45.0)
    # AFTER
    await client.sources.add_url(nb_id, url, wait=True, wait_timeout=45.0)
  • ArtifactsAPI.wait_for_completion(poll_interval=...) — the deprecated
    poll_interval alias was removed; use initial_interval=... (same
    cadence). Passing poll_interval raises TypeError.
    # BEFORE
    await client.artifacts.wait_for_completion(nb_id, task_id, poll_interval=5.0)
    # AFTER
    await client.artifacts.wait_for_completion(nb_id, task_id, initial_interval=5.0)
  • NOTEBOOKLM_STRICT_DECODE=0 soft-mode opt-out — removed. Strict
    decoding is now the only mode: schema-drift helpers (notably safe_index)
    always raise UnknownRPCMethodError on shape drift instead of
    warn-and-returning None / []. The env var is now ignored (no-op).
    Callers that previously relied on the soft fallback should handle
    UnknownRPCMethodError (a subclass of RPCError / DecodingError).
  • NotesAPI.create_from_chat(...) — removed (deprecated since v0.5.0,
    two MINOR cycles of warnings served; the documented removal target was
    v0.7.0). It was a pure forwarder. Use ChatAPI.save_answer_as_note(...),
    the canonical citation-rich saved-from-chat method and data owner
    (ADR-0013): await client.chat.save_answer_as_note(nb_id, ask_result).
    The now-unused save_chat_answer injection plumbing on NotesAPI was
    removed with it.

Not removed: SourcesAPI.add_file(mime_type=...) and
notebooklm source add --mime-type (file sources) were reassessed and
kept
mime_type was re-wired to set the resumable-upload content-type
header (overriding filename-extension inference), so it is a supported
parameter, not a dead shim. Its stale DeprecationWarning had already been
removed; the documentation now reflects this.

Not removed: awaiting NotebookLMClient.from_storage(...) still works —
its deprecation targets v1.0, not v0.6.0.

Added

  • get_or_none() — the sanctioned silent optional lookup, added to
    client.sources / client.artifacts / client.notes / client.mind_maps.
    It returns the entity (Source / Artifact / Note / MindMap) or None
    for a genuine absence and never warns, making it the drop-in migration
    target for the now-deprecated get()-returns-None pattern (see
    Deprecated below; issue #1247). Unlike get(), it does not swallow
    transport, auth, or decode faults — only a real "not found" yields None.
    # Silent optional lookup (no DeprecationWarning):
    src = await client.sources.get_or_none(nb_id, source_id)
    if src is None:
        ...
    Additive (ADR-0019; issue #1247).
  • NOTEBOOKLM_FUTURE_ERRORS opt-in preview flag — run the v0.8.0 error
    contract
    early to test forward-compatibility before the breaking flips ship
    (ADR-0019 / umbrella #1346). Default-off and byte-identical to current
    v0.7.0 behavior; when truthy (1/true/yes/on) the three warn-runways
    adopt their v0.8.0 raise-target: sources.get / artifacts.get /
    notes.get / mind_maps.get raise the matching *NotFoundError on a miss
    (#1247), the whole MappingCompatMixin mapping surface — [...]
    subscript plus the silent get / keys / items / values / len / in /
    iter shims — raises the exact error a bare dataclass would (#1251), and
    the deprecated ResearchAPI.wait_for_completion(interval=...) alias raises
    TypeError (#1254). Takes precedence over NOTEBOOKLM_QUIET_DEPRECATIONS
    (a runway raises regardless of quiet). The four get() methods are now routed
    through a single _lookup.resolve_get bridge, eliminating the hand-duplicated
    warn-on-miss pattern. Helper: notebooklm._deprecation.future_errors_enabled.
    The flag now also previews the purely-behavioral v0.8.0 changes that have
    no warn-runway (#1405): the uninformative bool returns of sources.refresh
    and chat.delete_conversation become None (#1290); a synchronous generation
    refusal raises the decoder's RateLimitError / RPCError /
    DecodingError / ArtifactFeatureUnavailableError instead of being swallowed
    into GenerationStatus(status="failed") / returned None — across
    _call_generate, revise_slide, _parse_generation_result, and
    research.start (#1342); and the mutate-existing ops notes.update and
    sources/artifacts rename(return_object=False) fail loud with a
    *NotFoundError on a missing target (#1362). These previews are runtime-only —
    no public return annotation changes until the v0.8.0 flip — so default-off
    stays byte-identical. Does not close #1247/#1251/#1254/#1290/#1342/#1362
    the runways and current behavior remain until the v0.8.0 flip. See
    docs/deprecations.md. Additive (issues #1346, #1405).
  • client.artifacts.retry_failed(notebook_id, artifact_id) — retry a failed
    Studio artifact in place (the web UI "Retry" action), via the new
    RETRY_ARTIFACT (Rytqqe) RPC. The artifact is not deleted first and the
    same artifact_id is preserved, so existing poll_status() /
    wait_for_completion() flows keep working. Follows the ADR-0019 "async
    kickoff" contract: an accepted retry returns
    GenerationStatus(status="in_progress"), while a synchronous refusal
    (USER_DISPLAYABLE_ERROR — rate limit / quota / not-retryable) raises the
    underlying RateLimitError / RPCError rather than returning a
    status="failed" handle. New notebooklm artifact retry <artifact_id> [--wait] [--json] CLI command. Additive (issues #1319, #1346).
  • notebooklm.artifacts.with_rate_limit_retry now also retries when the
    wrapped callable raises RateLimitError (backing off and re-raising once
    the retry budget is exhausted), so it can wrap the new retry_failed. The
    existing returned-rate-limited-GenerationStatus path (used by generate_*)
    is unchanged — this is a backward-compatible addition (issue #1319).
  • New public exception types for the note and mind-map domains, mirroring the
    existing SourceError / SourceNotFoundError shape: NoteError +
    NoteNotFoundError and MindMapError + MindMapNotFoundError. Each
    *NotFoundError is a triple-base (NotFoundError, RPCError, <Domain>Error),
    so it is catchable via the cross-domain NotFoundError umbrella, at
    transport-level except RPCError call sites, and at domain-level
    except NoteError / except MindMapError call sites. These are the
    prerequisite for the mind-map not-found work (ADR-0019; issues #1291, #1346).
    MindMapNotFoundError is now raised by the mind_maps mutation paths (see
    Changed below); NoteNotFoundError is not raised by any method yet.
  • ResearchStatus.NOT_FOUND — a typed lifecycle sentinel for the
    poll-observed absence of a specific requested research task, distinct from
    NO_RESEARCH ("nothing in flight"). research.poll(notebook_id, task_id=...)
    now returns ResearchTask.not_found(task_id) (status NOT_FOUND, carrying
    the requested id) when a non-empty pinned task_id matches no in-flight task;
    the unfiltered task_id=None empty poll still returns NO_RESEARCH
    unchanged. Additive and non-breaking — the poll never raises for an absent
    task (ADR-0019 Rule 4; issues #1344, #1346).
  • Typed return values for the research / mind-map / source-guide methods.
    research.poll / research.start / research.wait_for_completion,
    artifacts.generate_mind_map, and sources.get_guide now return typed
    dataclasses instead of untyped dict[str, Any], with a new
    ResearchStatus str-enum for the status field. The new public types are
    exported from notebooklm and notebooklm.types:
    ResearchStatus, ResearchTask, ResearchSource, ResearchStart,
    MindMapResult, and SourceGuide.
    from notebooklm import ResearchStatus
    
    result = await client.research.poll(nb_id)
    if result.status == ResearchStatus.COMPLETED:   # also == "completed"
        for source in result.sources:
            print(source.title, source.url)
    
    guide = await client.sources.get_guide(nb_id, src_id)
    print(guide.summary, guide.keywords)
    
    mind_map = await client.artifacts.generate_mind_map(nb_id)
    print(mind_map.note_id, mind_map.mind_map)
    This is backward-compatible: ResearchStatus is a str enum (so
    status == "completed" still holds), and the returned dataclasses keep
    working as read-only mappings — result["status"] / result.get("status")
    / result.keys() / "status" in result all still work (subscript emits a
    DeprecationWarning; see Deprecated below). The dict-subscript bridge is
    removed in v0.8.0.
  • WaitTimeoutError — one catchable base for every wait/poll timeout. A
    new public exception (notebooklm.WaitTimeoutError) is the common base of
    SourceTimeoutError, ArtifactTimeoutError (and its
    ArtifactPendingTimeoutError / ArtifactInProgressTimeoutError subclasses),
    and the new ResearchTimeoutError, so a single except WaitTimeoutError
    clause catches a wait timeout from any domain. It mixes in the built-in
    TimeoutError, so this is fully backward-compatible: existing
    except TimeoutError clauses keep catching every wait timeout unchanged.
    from notebooklm import WaitTimeoutError
    try:
        await client.sources.wait_until_ready(nb_id, src_id)
        await client.artifacts.wait_for_completion(nb_id, task_id)
        await client.research.wait_for_completion(nb_id, research_task_id)
    except WaitTimeoutError:   # was three separate / inconsistent timeout types
        ...
  • ResearchError / ResearchTimeoutError. The research domain gained a
    catchable base (ResearchError, mirroring SourceError / ArtifactError)
    and a domain timeout (ResearchTimeoutError). ResearchAPI.wait_for_completion
    previously raised the bare built-in TimeoutError; it now raises
    ResearchTimeoutError, a WaitTimeoutError (and therefore still a
    TimeoutError), exposing notebook_id / task_id / timeout /
    timeout_seconds / last_status. (ResearchTaskMismatchError stays a
    ValidationError — it is caller-input validation, not a wait timeout.)

Changed / Deprecated

  • ArtifactTimeoutError now declares its bases umbrella-first
    (WaitTimeoutError, ArtifactError), matching SourceTimeoutError and
    ResearchTimeoutError. This is a cosmetic reorder with no behavior change:
    isinstance/except against either base is unaffected.
  • client.mind_maps mutation sites now raise MindMapNotFoundError instead of
    a bare ValueError on a missing target, so callers can except NotFoundError
    (or except MindMapError) uniformly across namespaces. rename (and the
    underlying note-backed rename_mind_map) raise it; MindMapNotFoundError
    multi-inherits ValueError's sibling NotFoundError, not ValueError
    itself, so existing except ValueError rename callers must switch to
    except NotFoundError / except MindMapNotFoundError. delete(kind=None) is
    now idempotent — deleting an already-absent mind map returns None rather
    than raising (matching sources/artifacts/notes delete, and the
    kind-supplied path). get_tree returns None for a missing mind map (it is
    a derived read that does not police parent existence) — previously kind=None
    raised on an unknown id. Shape-drift in the interactive payload still raises
    UnknownRPCMethodError (ADR-0019; issues #1291, #1346).
  • client.mind_maps.generate(kind=MindMapKind.INTERACTIVE) now raises
    ArtifactFeatureUnavailableError (instead of a bare ArtifactError) when the
    CREATE_ARTIFACT call returns no artifact id — no generation task was
    created. Non-breaking for except ArtifactError:
    ArtifactFeatureUnavailableError is a subclass of ArtifactError, so that
    catch still works. (It also multi-inherits RPCError, so a handler that does
    except RPCError before except ArtifactError will now take the RPCError
    branch — the same MRO the sibling generate_* / retry_failed null-create
    paths already produce.) This aligns the interactive async kickoff with that
    sibling null-create contract (ADR-0019 "async kickoff"; issue #1359).
  • Documented two pre-existing client.mind_maps read semantics (docs-only, no
    behavior change): list() populates MindMap.tree only for note-backed
    entries — interactive entries carry tree=None ("not fetched", not "empty";
    call get_tree(..., kind=MindMapKind.INTERACTIVE) to fetch one); and the explicit
    get_tree(..., kind=MindMapKind.INTERACTIVE) path delegates absence detection to the RPC,
    so a missing id's value is server-dependent (returns None today) rather than
    enforced client-side (issues #1355, #1359).
  • ResearchAPI.wait_for_completion(interval=...)initial_interval=....
    The research waiter's poll-cadence keyword is now initial_interval,
    matching SourcesAPI.wait_until_ready and
    ArtifactsAPI.wait_for_completion. The old interval= keyword still works
    as a deprecated alias (warns in 0.7.0, removed in v0.8.0): passing a
    non-default value emits a DeprecationWarning (suppressible with
    NOTEBOOKLM_QUIET_DEPRECATIONS=1), and passing both interval and
    initial_interval raises TypeError. Default-shape calls stay silent and
    the signature is otherwise unchanged, so the public-API compatibility audit
    stays clean. See docs/deprecations.md for the
    migration.

    Decision — wait_timeout kept. The wait_timeout keyword on the
    SourcesAPI.add_* family was deliberately not renamed to timeout:
    on those add methods timeout would be ambiguous with a per-request HTTP
    timeout, whereas the dedicated waiter methods already spell their budget
    timeout. The research intervalinitial_interval rename was the only
    standardization with a clear, unambiguous win.

Deprecated

Every deprecation below is on a compatibility runway to v0.8.0. The
consolidated Upgrading to v0.8.0 guide is the
single reference for moving your code across the boundary; set
NOTEBOOKLM_FUTURE_ERRORS=1 to exercise the v0.8.0 behavior in your tests
today.

  • client.mind_maps.get() returning None for a missing mind map is now
    deprecated
    , closing the runway gap that left mind_maps as the only
    #1247-cohort namespace without one. It now emits a DeprecationWarning on a
    miss while still returning None (behavior unchanged this release),
    matching sources.get() / artifacts.get() / notes.get(). In v0.8.0 it
    will instead raise MindMapNotFoundError. Use get_or_none() for the
    sanctioned optional lookup (it stays silent), or migrate the None-check to a
    try/except MindMapNotFoundError. The warning fires only on a miss; suppress
    it with NOTEBOOKLM_QUIET_DEPRECATIONS=1. Tracking issue: #1247 (gap: #1358).
    See docs/deprecations.md.
  • sources.get() / artifacts.get() / notes.get() returning None for a
    missing entity is deprecated.
    These three methods now emit a
    DeprecationWarning on a miss while still returning None (behavior is
    unchanged this release). In v0.8.0 they will instead raise the
    matching *NotFoundError (SourceNotFoundError / ArtifactNotFoundError /
    NoteNotFoundError), unifying the not-found contract with notebooks.get(),
    which already raises NotebookNotFoundError. Tracking issue: #1247.
    # Migrate the None-check to a try/except before v0.8.0:
    # BEFORE (deprecated)
    src = await client.sources.get(nb_id, source_id)
    if src is None:
        ...
    # AFTER
    try:
        src = await client.sources.get(nb_id, source_id)
    except SourceNotFoundError:
        ...
    The warning fires only on a miss; successful lookups stay silent. Suppress it
    with NOTEBOOKLM_QUIET_DEPRECATIONS=1. See
    docs/deprecations.md.
  • Dict-subscript access on the new typed research / mind-map / guide
    returns is deprecated.
    Now that research.poll / research.start /
    research.wait_for_completion, artifacts.generate_mind_map, and
    sources.get_guide return typed dataclasses (see Added), the legacy
    result["status"] dict-subscript access emits a DeprecationWarning and
    will be removed in v0.8.0. Migrate to attribute access (result.status).
    The silent result.get(...) / result.keys() / "x" in result mapping
    shims also disappear in v0.8.0. Suppress the warning with
    NOTEBOOKLM_QUIET_DEPRECATIONS=1. See
    docs/deprecations.md.
    # BEFORE (still works in 0.7.0, warns on subscript)
    if result["status"] == "completed":
        sources = result["sources"]
    # AFTER
    if result.status == "completed":
        sources = result.sources

Fixed

  • CLI now emits the NOT_FOUND error envelope for the *NotFoundError
    family from the centralized handler, instead of the generic
    NOTEBOOKLM_ERROR.
    Any NotebookNotFoundError / SourceNotFoundError /
    ArtifactNotFoundError / NoteNotFoundError / MindMapNotFoundError that
    reaches cli/error_handler.py (e.g. notebooks.get() on a missing notebook,
    or a rename whose target was deleted mid-operation) now exits 1 with the
    typed {"error": true, "code": "NOT_FOUND", ...} JSON envelope carrying the
    missing resource id — matching the per-command source / artifact /
    note get convention (the documented CLI not-found contract since v0.5.0).
    The per-command get paths already used get_or_none and are unaffected.
    This also makes the NOTEBOOKLM_FUTURE_ERRORS=1 preview faithful at the CLI
    boundary, pre-positioning it for the v0.8.0 get() → raise / mutate-existing
    fail-loud flips (issues #1364, #1247, #1362).
  • Source.from_api_response now reports the real processing status. The
    ADD_SOURCE / rename parsing path previously never read the status block and
    always fell back to SourceStatus.READY, while client.sources.list() /
    get() and the source poller read the decoded status. Both parsers now
    funnel through a single Source.from_row construction site, so a Source
    produced from an add/rename response carries the same status (and url /
    created_at) as the listing path. The Source.status field annotation was
    also corrected from int to SourceStatus (still an int-compatible enum).

Don't miss a new notebooklm-py release

NewReleases is sending notifications on new releases.