Skip to content

API Client

Client for the Todoist API.

Provides methods for interacting with Todoist resources like tasks, projects, labels, comments, etc.

Manages an HTTP session and handles authentication. Can be used as a context manager to ensure the session is closed properly.

Source code in todoist_api_python/api.py
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
class TodoistAPI:
    """
    Client for the Todoist API.

    Provides methods for interacting with Todoist resources like tasks, projects,
    labels, comments, etc.

    Manages an HTTP session and handles authentication. Can be used as a context manager
    to ensure the session is closed properly.
    """

    def __init__(self, token: str, session: requests.Session | None = None) -> None:
        """
        Initialize the TodoistAPI client.

        :param token: Authentication token for the Todoist API.
        :param session: An optional pre-configured requests `Session` object.
        """
        self._token: str = token
        self._session = session or requests.Session()
        self._finalizer = finalize(self, self._session.close)

    def __enter__(self) -> Self:
        """
        Enters the runtime context related to this object.

        The with statement will bind this method's return value to the target(s)
        specified in the as clause of the statement, if any.

        :return: The TodoistAPI instance.
        :rtype: Self
        """
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc_value: BaseException | None,
        traceback: TracebackType | None,
    ) -> None:
        """Exit the runtime context and closes the underlying requests session."""
        self._finalizer()

    def get_task(self, task_id: str) -> Task:
        """
        Get a specific task by its ID.

        :param task_id: The ID of the task to retrieve.
        :return: The requested task.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response is not a valid Task dictionary.
        """
        endpoint = get_api_url(f"{TASKS_PATH}/{task_id}")
        task_data: dict[str, Any] = get(self._session, endpoint, self._token)
        return Task.from_dict(task_data)

    def get_tasks(
        self,
        *,
        project_id: str | None = None,
        section_id: str | None = None,
        parent_id: str | None = None,
        label: str | None = None,
        ids: list[str] | None = None,
        limit: Annotated[int, Ge(1), Le(200)] | None = None,
    ) -> Iterator[list[Task]]:
        """
        Get an iterable of lists of active tasks.

        The response is an iterable of lists of active tasks matching the criteria.
        Be aware that each iteration fires off a network request to the Todoist API,
        and may result in rate limiting or other API restrictions.

        :param project_id: Filter tasks by project ID.
        :param section_id: Filter tasks by section ID.
        :param parent_id: Filter tasks by parent task ID.
        :param label: Filter tasks by label name.
        :param ids: A list of the IDs of the tasks to retrieve.
        :param limit: Maximum number of tasks per page.
        :return: An iterable of lists of tasks.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response structure is unexpected.
        """
        endpoint = get_api_url(TASKS_PATH)

        params: dict[str, Any] = {}
        if project_id is not None:
            params["project_id"] = project_id
        if section_id is not None:
            params["section_id"] = section_id
        if parent_id is not None:
            params["parent_id"] = parent_id
        if label is not None:
            params["label"] = label
        if ids is not None:
            params["ids"] = ",".join(str(i) for i in ids)
        if limit is not None:
            params["limit"] = limit

        return ResultsPaginator(
            self._session,
            endpoint,
            "results",
            Task.from_dict,
            self._token,
            params,
        )

    def filter_tasks(
        self,
        *,
        query: Annotated[str, MaxLen(1024)] | None = None,
        lang: str | None = None,
        limit: Annotated[int, Ge(1), Le(200)] | None = None,
    ) -> Iterator[list[Task]]:
        """
        Get an iterable of lists of active tasks matching the filter.

        The response is an iterable of lists of active tasks matching the criteria.
        Be aware that each iteration fires off a network request to the Todoist API,
        and may result in rate limiting or other API restrictions.

        :param query: Query tasks using Todoist's filter language.
        :param lang: Language for task content (e.g., 'en').
        :param limit: Maximum number of tasks per page.
        :return: An iterable of lists of tasks.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response structure is unexpected.
        """
        endpoint = get_api_url(TASKS_FILTER_PATH)

        params: dict[str, Any] = {}
        if query is not None:
            params["query"] = query
        if lang is not None:
            params["lang"] = lang
        if limit is not None:
            params["limit"] = limit

        return ResultsPaginator(
            self._session,
            endpoint,
            "results",
            Task.from_dict,
            self._token,
            params,
        )

    def add_task(  # noqa: PLR0912
        self,
        content: Annotated[str, MinLen(1), MaxLen(500)],
        *,
        description: Annotated[str, MaxLen(16383)] | None = None,
        project_id: str | None = None,
        section_id: str | None = None,
        parent_id: str | None = None,
        labels: list[Annotated[str, MaxLen(100)]] | None = None,
        priority: Annotated[int, Ge(1), Le(4)] | None = None,
        due_string: Annotated[str, MaxLen(150)] | None = None,
        due_lang: LanguageCode | None = None,
        due_date: date | None = None,
        due_datetime: datetime | None = None,
        assignee_id: str | None = None,
        order: int | None = None,
        auto_reminder: bool | None = None,
        auto_parse_labels: bool | None = None,
        duration: Annotated[int, Ge(1)] | None = None,
        duration_unit: Literal["minute", "day"] | None = None,
        deadline_date: date | None = None,
        deadline_lang: LanguageCode | None = None,
    ) -> Task:
        """
        Create a new task.

        :param content: The text content of the task.
        :param project_id: The ID of the project to add the task to.
        :param section_id: The ID of the section to add the task to.
        :param parent_id: The ID of the parent task.
        :param labels: The task's labels (a list of names).
        :param priority: The priority of the task (4 for very urgent).
        :param due_string: The due date in natural language format.
        :param due_lang: Language for parsing the due date (e.g., 'en').
        :param due_date: The due date as a date object.
        :param due_datetime: The due date and time as a datetime object.
        :param assignee_id: User ID to whom the task is assigned.
        :param description: Description for the task.
        :param order: The order of task in the project or section.
        :param auto_reminder: Whether to add default reminder if date with time is set.
        :param auto_parse_labels: Whether to parse labels from task content.
        :param duration: The amount of time the task will take.
        :param duration_unit: The unit of time for duration.
        :param deadline_date: The deadline date as a date object.
        :param deadline_lang: Language for parsing the deadline date.
        :return: The newly created task.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response is not a valid Task dictionary.
        """
        endpoint = get_api_url(TASKS_PATH)

        data: dict[str, Any] = {"content": content}
        if description is not None:
            data["description"] = description
        if project_id is not None:
            data["project_id"] = project_id
        if section_id is not None:
            data["section_id"] = section_id
        if parent_id is not None:
            data["parent_id"] = parent_id
        if labels is not None:
            data["labels"] = labels
        if priority is not None:
            data["priority"] = priority
        if due_string is not None:
            data["due_string"] = due_string
        if due_lang is not None:
            data["due_lang"] = due_lang
        if due_date is not None:
            data["due_date"] = format_date(due_date)
        if due_datetime is not None:
            data["due_datetime"] = format_datetime(due_datetime)
        if assignee_id is not None:
            data["assignee_id"] = assignee_id
        if order is not None:
            data["order"] = order
        if auto_reminder is not None:
            data["auto_reminder"] = auto_reminder
        if auto_parse_labels is not None:
            data["auto_parse_labels"] = auto_parse_labels
        if duration is not None:
            data["duration"] = duration
        if duration_unit is not None:
            data["duration_unit"] = duration_unit
        if deadline_date is not None:
            data["deadline_date"] = format_date(deadline_date)
        if deadline_lang is not None:
            data["deadline_lang"] = deadline_lang

        task_data: dict[str, Any] = post(
            self._session, endpoint, self._token, data=data
        )
        return Task.from_dict(task_data)

    def add_task_quick(
        self,
        text: str,
        *,
        note: str | None = None,
        reminder: str | None = None,
        auto_reminder: bool = True,
    ) -> Task:
        """
        Create a new task using Todoist's Quick Add syntax.

        This automatically parses dates, deadlines, projects, labels, priorities, etc,
        from the provided text (e.g., "Buy milk #Shopping @groceries tomorrow p1").

        :param text: The task text using Quick Add syntax.
        :param note: Optional note to be added to the task.
        :param reminder: Optional reminder date in free form text.
        :param auto_reminder: Whether to add default reminder if date with time is set.
        :return: A result object containing the parsed task data and metadata.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response cannot be parsed into a QuickAddResult.
        """
        endpoint = get_api_url(TASKS_QUICK_ADD_PATH)

        data = {
            "meta": True,
            "text": text,
            "auto_reminder": auto_reminder,
        }

        if note is not None:
            data["note"] = note
        if reminder is not None:
            data["reminder"] = reminder

        task_data: dict[str, Any] = post(
            self._session, endpoint, self._token, data=data
        )
        return Task.from_dict(task_data)

    def update_task(  # noqa: PLR0912
        self,
        task_id: str,
        *,
        content: Annotated[str, MinLen(1), MaxLen(500)] | None = None,
        description: Annotated[str, MaxLen(16383)] | None = None,
        labels: list[Annotated[str, MaxLen(60)]] | None = None,
        priority: Annotated[int, Ge(1), Le(4)] | None = None,
        due_string: Annotated[str, MaxLen(150)] | None = None,
        due_lang: LanguageCode | None = None,
        due_date: date | None = None,
        due_datetime: datetime | None = None,
        assignee_id: str | None = None,
        day_order: int | None = None,
        collapsed: bool | None = None,
        duration: Annotated[int, Ge(1)] | None = None,
        duration_unit: Literal["minute", "day"] | None = None,
        deadline_date: date | None = None,
        deadline_lang: LanguageCode | None = None,
    ) -> Task:
        """
        Update an existing task.

        Only the fields to be updated need to be provided.

        :param task_id: The ID of the task to update.
        :param content: The text content of the task.
        :param description: Description for the task.
        :param labels: The task's labels (a list of names).
        :param priority: The priority of the task (4 for very urgent).
        :param due_string: The due date in natural language format.
        :param due_lang: Language for parsing the due date (e.g., 'en').
        :param due_date: The due date as a date object.
        :param due_datetime: The due date and time as a datetime object.
        :param assignee_id: User ID to whom the task is assigned.
        :param day_order: The order of the task inside Today or Next 7 days view.
        :param collapsed: Whether the task's sub-tasks are collapsed.
        :param duration: The amount of time the task will take.
        :param duration_unit: The unit of time for duration.
        :param deadline_date: The deadline date as a date object.
        :param deadline_lang: Language for parsing the deadline date.
        :return: the updated Task.
        :raises requests.exceptions.HTTPError: If the API request fails.
        """
        endpoint = get_api_url(f"{TASKS_PATH}/{task_id}")

        data: dict[str, Any] = {}
        if content is not None:
            data["content"] = content
        if description is not None:
            data["description"] = description
        if labels is not None:
            data["labels"] = labels
        if priority is not None:
            data["priority"] = priority
        if due_string is not None:
            data["due_string"] = due_string
        if due_lang is not None:
            data["due_lang"] = due_lang
        if due_date is not None:
            data["due_date"] = format_date(due_date)
        if due_datetime is not None:
            data["due_datetime"] = format_datetime(due_datetime)
        if assignee_id is not None:
            data["assignee_id"] = assignee_id
        if day_order is not None:
            data["day_order"] = day_order
        if collapsed is not None:
            data["collapsed"] = collapsed
        if duration is not None:
            data["duration"] = duration
        if duration_unit is not None:
            data["duration_unit"] = duration_unit
        if deadline_date is not None:
            data["deadline_date"] = format_date(deadline_date)
        if deadline_lang is not None:
            data["deadline_lang"] = deadline_lang

        task_data: dict[str, Any] = post(
            self._session, endpoint, self._token, data=data
        )
        return Task.from_dict(task_data)

    def complete_task(self, task_id: str) -> bool:
        """
        Complete a task.

        For recurring tasks, this schedules the next occurrence.
        For non-recurring tasks, it marks them as completed.

        :param task_id: The ID of the task to close.
        :return: True if the task was closed successfully,
                 False otherwise (possibly raise `HTTPError` instead).
        :raises requests.exceptions.HTTPError: If the API request fails.
        """
        endpoint = get_api_url(f"{TASKS_PATH}/{task_id}/close")
        return post(self._session, endpoint, self._token)

    def uncomplete_task(self, task_id: str) -> bool:
        """
        Uncomplete a (completed) task.

        Any parent tasks or sections will also be uncompleted.

        :param task_id: The ID of the task to reopen.
        :return: True if the task was uncompleted successfully,
                 False otherwise (possibly raise `HTTPError` instead).
        :rtype: bool
        :raises requests.exceptions.HTTPError: If the API request fails.
        """
        endpoint = get_api_url(f"{TASKS_PATH}/{task_id}/reopen")
        return post(self._session, endpoint, self._token)

    def delete_task(self, task_id: str) -> bool:
        """
        Delete a task.

        :param task_id: The ID of the task to delete.
        :return: True if the task was deleted successfully,
                 False otherwise (possibly raise `HTTPError` instead).
        :raises requests.exceptions.HTTPError: If the API request fails.
        """
        endpoint = get_api_url(f"{TASKS_PATH}/{task_id}")
        return delete(self._session, endpoint, self._token)

    def get_completed_tasks_by_due_date(
        self,
        *,
        since: datetime,
        until: datetime,
        workspace_id: str | None = None,
        project_id: str | None = None,
        section_id: str | None = None,
        parent_id: str | None = None,
        filter_query: str | None = None,
        filter_lang: str | None = None,
        limit: Annotated[int, Ge(1), Le(200)] | None = None,
    ) -> Iterator[list[Task]]:
        """
        Get an iterable of lists of completed tasks within a due date range.

        Retrieves tasks completed within a specific due date range (up to 6 weeks).
        Supports filtering by workspace, project, section, parent task, or a query.

        The response is an iterable of lists of completed tasks. Be aware that each
        iteration fires off a network request to the Todoist API, and may result in
        rate limiting or other API restrictions.

        :param since: Start of the date range (inclusive).
        :param until: End of the date range (inclusive).
        :param workspace_id: Filter by workspace ID.
        :param project_id: Filter by project ID.
        :param section_id: Filter by section ID.
        :param parent_id: Filter by parent task ID.
        :param filter_query: Filter by a query string.
        :param filter_lang: Language for the filter query (e.g., 'en').
        :param limit: Maximum number of tasks per page (default 50).
        :return: An iterable of lists of completed tasks.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response structure is unexpected.
        """
        endpoint = get_api_url(TASKS_COMPLETED_BY_DUE_DATE_PATH)

        params: dict[str, Any] = {
            "since": format_datetime(since),
            "until": format_datetime(until),
        }
        if workspace_id is not None:
            params["workspace_id"] = workspace_id
        if project_id is not None:
            params["project_id"] = project_id
        if section_id is not None:
            params["section_id"] = section_id
        if parent_id is not None:
            params["parent_id"] = parent_id
        if filter_query is not None:
            params["filter_query"] = filter_query
        if filter_lang is not None:
            params["filter_lang"] = filter_lang
        if limit is not None:
            params["limit"] = limit

        return ResultsPaginator(
            self._session, endpoint, "items", Task.from_dict, self._token, params
        )

    def get_completed_tasks_by_completion_date(
        self,
        *,
        since: datetime,
        until: datetime,
        workspace_id: str | None = None,
        filter_query: str | None = None,
        filter_lang: str | None = None,
        limit: Annotated[int, Ge(1), Le(200)] | None = None,
    ) -> Iterator[list[Task]]:
        """
        Get an iterable of lists of completed tasks within a date range.

        Retrieves tasks completed within a specific date range (up to 3 months).
        Supports filtering by workspace or a filter query.

        The response is an iterable of lists of completed tasks. Be aware that each
        iteration fires off a network request to the Todoist API, and may result in
        rate limiting or other API restrictions.

        :param since: Start of the date range (inclusive).
        :param until: End of the date range (inclusive).
        :param workspace_id: Filter by workspace ID.
        :param filter_query: Filter by a query string.
        :param filter_lang: Language for the filter query (e.g., 'en').
        :param limit: Maximum number of tasks per page (default 50).
        :return: An iterable of lists of completed tasks.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response structure is unexpected.
        """
        endpoint = get_api_url(TASKS_COMPLETED_BY_COMPLETION_DATE_PATH)

        params: dict[str, Any] = {
            "since": format_datetime(since),
            "until": format_datetime(until),
        }
        if workspace_id is not None:
            params["workspace_id"] = workspace_id
        if filter_query is not None:
            params["filter_query"] = filter_query
        if filter_lang is not None:
            params["filter_lang"] = filter_lang
        if limit is not None:
            params["limit"] = limit

        return ResultsPaginator(
            self._session, endpoint, "items", Task.from_dict, self._token, params
        )

    def get_project(self, project_id: str) -> Project:
        """
        Get a project by its ID.

        :param project_id: The ID of the project to retrieve.
        :return: The requested project.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response is not a valid Project dictionary.
        """
        endpoint = get_api_url(f"{PROJECTS_PATH}/{project_id}")
        project_data: dict[str, Any] = get(self._session, endpoint, self._token)
        return Project.from_dict(project_data)

    def get_projects(
        self,
        limit: Annotated[int, Ge(1), Le(200)] | None = None,
    ) -> Iterator[list[Project]]:
        """
        Get an iterable of lists of active projects.

        The response is an iterable of lists of active projects.
        Be aware that each iteration fires off a network request to the Todoist API,
        and may result in rate limiting or other API restrictions.

        :param limit: Maximum number of projects per page.
        :return: An iterable of lists of projects.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response structure is unexpected.
        """
        endpoint = get_api_url(PROJECTS_PATH)
        params: dict[str, Any] = {}
        if limit is not None:
            params["limit"] = limit
        return ResultsPaginator(
            self._session, endpoint, "results", Project.from_dict, self._token, params
        )

    def add_project(
        self,
        name: Annotated[str, MinLen(1), MaxLen(120)],
        *,
        description: Annotated[str, MaxLen(16383)] | None = None,
        parent_id: str | None = None,
        color: ColorString | None = None,
        is_favorite: bool | None = None,
        view_style: ViewStyle | None = None,
    ) -> Project:
        """
        Create a new project.

        :param name: The name of the project.
        :param description: Description for the project (up to 1024 characters).
        :param parent_id: The ID of the parent project. Set to null for root projects.
        :param color: The color of the project icon.
        :param is_favorite: Whether the project is a favorite.
        :param view_style: A string value (either 'list' or 'board', default is 'list').
        :return: The newly created project.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response is not a valid Project dictionary.
        """
        endpoint = get_api_url(PROJECTS_PATH)

        data: dict[str, Any] = {"name": name}
        if parent_id is not None:
            data["parent_id"] = parent_id
        if description is not None:
            data["description"] = description
        if color is not None:
            data["color"] = color
        if is_favorite is not None:
            data["is_favorite"] = is_favorite
        if view_style is not None:
            data["view_style"] = view_style

        project_data: dict[str, Any] = post(
            self._session, endpoint, self._token, data=data
        )
        return Project.from_dict(project_data)

    def update_project(
        self,
        project_id: str,
        *,
        name: Annotated[str, MinLen(1), MaxLen(120)] | None = None,
        description: Annotated[str, MaxLen(16383)] | None = None,
        color: ColorString | None = None,
        is_favorite: bool | None = None,
        view_style: ViewStyle | None = None,
    ) -> Project:
        """
        Update an existing project.

        Only the fields to be updated need to be provided as keyword arguments.

        :param project_id: The ID of the project to update.
        :param name: The name of the project.
        :param description: Description for the project (up to 1024 characters).
        :param color: The color of the project icon.
        :param is_favorite: Whether the project is a favorite.
        :param view_style: A string value (either 'list' or 'board').
        :return: the updated Project.
        :raises requests.exceptions.HTTPError: If the API request fails.
        """
        endpoint = get_api_url(f"{PROJECTS_PATH}/{project_id}")

        data: dict[str, Any] = {}

        if name is not None:
            data["name"] = name
        if description is not None:
            data["description"] = description
        if color is not None:
            data["color"] = color
        if is_favorite is not None:
            data["is_favorite"] = is_favorite
        if view_style is not None:
            data["view_style"] = view_style

        project_data: dict[str, Any] = post(
            self._session, endpoint, self._token, data=data
        )
        return Project.from_dict(project_data)

    def archive_project(self, project_id: str) -> Project:
        """
        Archive a project.

        For personal projects, archives it only for the user.
        For workspace projects, archives it for all members.

        :param project_id: The ID of the project to archive.
        :return: The archived project object.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response is not a valid Project dictionary.
        """
        endpoint = get_api_url(
            f"{PROJECTS_PATH}/{project_id}/{PROJECT_ARCHIVE_PATH_SUFFIX}"
        )
        project_data: dict[str, Any] = post(self._session, endpoint, self._token)
        return Project.from_dict(project_data)

    def unarchive_project(self, project_id: str) -> Project:
        """
        Unarchive a project.

        Restores a previously archived project.

        :param project_id: The ID of the project to unarchive.
        :return: The unarchived project object.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response is not a valid Project dictionary.
        """
        endpoint = get_api_url(
            f"{PROJECTS_PATH}/{project_id}/{PROJECT_UNARCHIVE_PATH_SUFFIX}"
        )
        project_data: dict[str, Any] = post(self._session, endpoint, self._token)
        return Project.from_dict(project_data)

    def delete_project(self, project_id: str) -> bool:
        """
        Delete a project.

        All nested sections and tasks will also be deleted.

        :param project_id: The ID of the project to delete.
        :return: True if the project was deleted successfully,
                 False otherwise (possibly raise `HTTPError` instead).
        :raises requests.exceptions.HTTPError: If the API request fails.
        """
        endpoint = get_api_url(f"{PROJECTS_PATH}/{project_id}")
        return delete(self._session, endpoint, self._token)

    def get_collaborators(
        self,
        project_id: str,
        limit: Annotated[int, Ge(1), Le(200)] | None = None,
    ) -> Iterator[list[Collaborator]]:
        """
        Get an iterable of lists of collaborators in shared projects.

        The response is an iterable of lists of collaborators in shared projects,
        Be aware that each iteration fires off a network request to the Todoist API,
        and may result in rate limiting or other API restrictions.

        :param project_id: The ID of the project.
        :param limit: Maximum number of collaborators per page.
        :return: An iterable of lists of collaborators.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response structure is unexpected.
        """
        endpoint = get_api_url(f"{PROJECTS_PATH}/{project_id}/{COLLABORATORS_PATH}")
        params: dict[str, Any] = {}
        if limit is not None:
            params["limit"] = limit
        return ResultsPaginator(
            self._session,
            endpoint,
            "results",
            Collaborator.from_dict,
            self._token,
            params,
        )

    def get_section(self, section_id: str) -> Section:
        """
        Get a specific section by its ID.

        :param section_id: The ID of the section to retrieve.
        :return: The requested section.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response is not a valid Section dictionary.
        """
        endpoint = get_api_url(f"{SECTIONS_PATH}/{section_id}")
        section_data: dict[str, Any] = get(self._session, endpoint, self._token)
        return Section.from_dict(section_data)

    def get_sections(
        self,
        project_id: str | None = None,
        *,
        limit: Annotated[int, Ge(1), Le(200)] | None = None,
    ) -> Iterator[list[Section]]:
        """
        Get an iterable of lists of active sections.

        Supports filtering by `project_id` and pagination arguments.

        The response is an iterable of lists of active sections.
        Be aware that each iteration fires off a network request to the Todoist API,
        and may result in rate limiting or other API restrictions.

        :param project_id: Filter sections by project ID.
        :param limit: Maximum number of sections per page.
        :return: An iterable of lists of sections.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response structure is unexpected.
        """
        endpoint = get_api_url(SECTIONS_PATH)

        params: dict[str, Any] = {}
        if project_id is not None:
            params["project_id"] = project_id
        if limit is not None:
            params["limit"] = limit

        return ResultsPaginator(
            self._session, endpoint, "results", Section.from_dict, self._token, params
        )

    def add_section(
        self,
        name: Annotated[str, MinLen(1), MaxLen(2048)],
        project_id: str,
        *,
        order: int | None = None,
    ) -> Section:
        """
        Create a new section within a project.

        :param name: The name of the section.
        :param project_id: The ID of the project to add the section to.
        :param order: The order of the section among all sections in the project.
        :return: The newly created section.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response is not a valid Section dictionary.
        """
        endpoint = get_api_url(SECTIONS_PATH)

        data: dict[str, Any] = {"name": name, "project_id": project_id}
        if order is not None:
            data["order"] = order

        section_data: dict[str, Any] = post(
            self._session, endpoint, self._token, data=data
        )
        return Section.from_dict(section_data)

    def update_section(
        self,
        section_id: str,
        name: Annotated[str, MinLen(1), MaxLen(2048)],
    ) -> Section:
        """
        Update an existing section.

        Currently, only `name` can be updated.

        :param section_id: The ID of the section to update.
        :param name: The new name for the section.
        :return: the updated Section.
        :raises requests.exceptions.HTTPError: If the API request fails.
        """
        endpoint = get_api_url(f"{SECTIONS_PATH}/{section_id}")
        section_data: dict[str, Any] = post(
            self._session, endpoint, self._token, data={"name": name}
        )
        return Section.from_dict(section_data)

    def delete_section(self, section_id: str) -> bool:
        """
        Delete a section.

        All tasks within the section will also be deleted.

        :param section_id: The ID of the section to delete.
        :return: True if the section was deleted successfully,
                 False otherwise (possibly raise `HTTPError` instead).
        :raises requests.exceptions.HTTPError: If the API request fails.
        """
        endpoint = get_api_url(f"{SECTIONS_PATH}/{section_id}")
        return delete(self._session, endpoint, self._token)

    def get_comment(self, comment_id: str) -> Comment:
        """
        Get a specific comment by its ID.

        :param comment_id: The ID of the comment to retrieve.
        :return: The requested comment.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response is not a valid Comment dictionary.
        """
        endpoint = get_api_url(f"{COMMENTS_PATH}/{comment_id}")
        comment_data: dict[str, Any] = get(self._session, endpoint, self._token)
        return Comment.from_dict(comment_data)

    def get_comments(
        self,
        *,
        project_id: str | None = None,
        task_id: str | None = None,
        limit: Annotated[int, Ge(1), Le(200)] | None = None,
    ) -> Iterator[list[Comment]]:
        """
        Get an iterable of lists of comments for a task or project.

        Requires either `project_id` or `task_id` to be set.

        The response is an iterable of lists of comments.
        Be aware that each iteration fires off a network request to the Todoist API,
        and may result in rate limiting or other API restrictions.

        :param project_id: The ID of the project to retrieve comments for.
        :param task_id: The ID of the task to retrieve comments for.
        :param limit: Maximum number of comments per page.
        :return: An iterable of lists of comments.
        :raises ValueError: If neither `project_id` nor `task_id` is provided.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response structure is unexpected.
        """
        if project_id is None and task_id is None:
            raise ValueError("Either `project_id` or `task_id` must be provided.")

        endpoint = get_api_url(COMMENTS_PATH)

        params: dict[str, Any] = {}
        if project_id is not None:
            params["project_id"] = project_id
        if task_id is not None:
            params["task_id"] = task_id
        if limit is not None:
            params["limit"] = limit

        return ResultsPaginator(
            self._session, endpoint, "results", Comment.from_dict, self._token, params
        )

    def add_comment(
        self,
        content: Annotated[str, MaxLen(15000)],
        *,
        project_id: str | None = None,
        task_id: str | None = None,
        attachment: Attachment | None = None,
        uids_to_notify: list[str] | None = None,
    ) -> Comment:
        """
        Create a new comment on a task or project.

        Requires either `project_id` or `task_id` to be set,
        and can optionally include an `attachment` object.

        :param content: The text content of the comment (supports Markdown).
        :param project_id: The ID of the project to add the comment to.
        :param task_id: The ID of the task to add the comment to.
        :param attachment: The attachment object to include with the comment.
        :param uids_to_notify: A list of user IDs to notify.
        :return: The newly created comment.
        :raises ValueError: If neither `project_id` nor `task_id` is provided.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response is not a valid Comment dictionary.
        """
        if project_id is None and task_id is None:
            raise ValueError("Either `project_id` or `task_id` must be provided.")

        endpoint = get_api_url(COMMENTS_PATH)

        data: dict[str, Any] = {"content": content}
        if project_id is not None:
            data["project_id"] = project_id
        if task_id is not None:
            data["task_id"] = task_id
        if attachment is not None:
            data["attachment"] = attachment.to_dict()
        if uids_to_notify is not None:
            data["uids_to_notify"] = uids_to_notify

        comment_data: dict[str, Any] = post(
            self._session, endpoint, self._token, data=data
        )
        return Comment.from_dict(comment_data)

    def update_comment(
        self, comment_id: str, content: Annotated[str, MaxLen(15000)]
    ) -> Comment:
        """
        Update an existing comment.

        Currently, only `content` can be updated.

        :param comment_id: The ID of the comment to update.
        :param content: The new text content for the comment.
        :return: the updated Comment.
        :raises requests.exceptions.HTTPError: If the API request fails.
        """
        endpoint = get_api_url(f"{COMMENTS_PATH}/{comment_id}")
        comment_data: dict[str, Any] = post(
            self._session, endpoint, self._token, data={"content": content}
        )
        return Comment.from_dict(comment_data)

    def delete_comment(self, comment_id: str) -> bool:
        """
        Delete a comment.

        :param comment_id: The ID of the comment to delete.
        :return: True if the comment was deleted successfully,
                 False otherwise (possibly raise `HTTPError` instead).
        :raises requests.exceptions.HTTPError: If the API request fails.
        """
        endpoint = get_api_url(f"{COMMENTS_PATH}/{comment_id}")
        return delete(self._session, endpoint, self._token)

    def get_label(self, label_id: str) -> Label:
        """
        Get a specific personal label by its ID.

        :param label_id: The ID of the label to retrieve.
        :return: The requested label.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response is not a valid Label dictionary.
        """
        endpoint = get_api_url(f"{LABELS_PATH}/{label_id}")
        label_data: dict[str, Any] = get(self._session, endpoint, self._token)
        return Label.from_dict(label_data)

    def get_labels(
        self,
        *,
        limit: Annotated[int, Ge(1), Le(200)] | None = None,
    ) -> Iterator[list[Label]]:
        """
        Get an iterable of lists of personal labels.

        Supports pagination arguments.

        The response is an iterable of lists of personal labels.
        Be aware that each iteration fires off a network request to the Todoist API,
        and may result in rate limiting or other API restrictions.

        :param limit: ` number of labels per page.
        :return: An iterable of lists of personal labels.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response structure is unexpected.
        """
        endpoint = get_api_url(LABELS_PATH)

        params: dict[str, Any] = {}
        if limit is not None:
            params["limit"] = limit

        return ResultsPaginator(
            self._session, endpoint, "results", Label.from_dict, self._token, params
        )

    def add_label(
        self,
        name: Annotated[str, MinLen(1), MaxLen(60)],
        *,
        color: ColorString | None = None,
        item_order: int | None = None,
        is_favorite: bool | None = None,
    ) -> Label:
        """
        Create a new personal label.

        :param name: The name of the label.
        :param color: The color of the label icon.
        :param item_order: Label's order in the label list.
        :param is_favorite: Whether the label is a favorite.
        :return: The newly created label.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response is not a valid Label dictionary.
        """
        endpoint = get_api_url(LABELS_PATH)

        data: dict[str, Any] = {"name": name}

        if color is not None:
            data["color"] = color
        if item_order is not None:
            data["item_order"] = item_order
        if is_favorite is not None:
            data["is_favorite"] = is_favorite

        label_data: dict[str, Any] = post(
            self._session, endpoint, self._token, data=data
        )
        return Label.from_dict(label_data)

    def update_label(
        self,
        label_id: str,
        *,
        name: Annotated[str, MinLen(1), MaxLen(60)] | None = None,
        color: ColorString | None = None,
        item_order: int | None = None,
        is_favorite: bool | None = None,
    ) -> Label:
        """
        Update a personal label.

        Only the fields to be updated need to be provided as keyword arguments.

        :param label_id: The ID of the label.
        :param name: The name of the label.
        :param color: The color of the label icon.
        :param item_order: Label's order in the label list.
        :param is_favorite: Whether the label is a favorite.
        :return: the updated Label.
        :raises requests.exceptions.HTTPError: If the API request fails.
        """
        endpoint = get_api_url(f"{LABELS_PATH}/{label_id}")

        data: dict[str, Any] = {}
        if name is not None:
            data["name"] = name
        if color is not None:
            data["color"] = color
        if item_order is not None:
            data["item_order"] = item_order
        if is_favorite is not None:
            data["is_favorite"] = is_favorite

        label_data: dict[str, Any] = post(
            self._session, endpoint, self._token, data=data
        )
        return Label.from_dict(label_data)

    def delete_label(self, label_id: str) -> bool:
        """
        Delete a personal label.

        Instances of the label will be removed from tasks.

        :param label_id: The ID of the label to delete.
        :return: True if the label was deleted successfully,
                 False otherwise (possibly raise `HTTPError` instead).
        :raises requests.exceptions.HTTPError: If the API request fails.
        """
        endpoint = get_api_url(f"{LABELS_PATH}/{label_id}")
        return delete(self._session, endpoint, self._token)

    def get_shared_labels(
        self,
        *,
        omit_personal: bool = False,
        limit: Annotated[int, Ge(1), Le(200)] | None = None,
    ) -> Iterator[list[str]]:
        """
        Get an iterable of lists of shared label names.

        Includes labels from collaborators on shared projects that are not in the
        user's personal labels. Can optionally exclude personal label names using
        `omit_personal=True`. Supports pagination arguments.

        The response is an iterable of lists of shared label names.
        Be aware that each iteration fires off a network request to the Todoist API,
        and may result in rate limiting or other API restrictions.

        :param omit_personal: Optional boolean flag to omit personal label names.
        :param limit: Maximum number of labels per page.
        :return: An iterable of lists of shared label names (strings).
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response structure is unexpected.
        """
        endpoint = get_api_url(SHARED_LABELS_PATH)

        params: dict[str, Any] = {"omit_personal": omit_personal}
        if limit is not None:
            params["limit"] = limit

        return ResultsPaginator(
            self._session, endpoint, "results", str, self._token, params
        )

    def rename_shared_label(
        self,
        name: Annotated[str, MaxLen(60)],
        new_name: Annotated[str, MinLen(1), MaxLen(60)],
    ) -> bool:
        """
        Rename all occurrences of a shared label across all projects.

        :param name: The current name of the shared label to rename.
        :param new_name: The new name for the shared label.
        :return: True if the rename was successful,
                 False otherwise (possibly raise `HTTPError` instead).
        :raises requests.exceptions.HTTPError: If the API request fails.
        """
        endpoint = get_api_url(SHARED_LABELS_RENAME_PATH)
        return post(
            self._session,
            endpoint,
            self._token,
            params={"name": name},
            data={"new_name": new_name},
        )

    def remove_shared_label(self, name: Annotated[str, MaxLen(60)]) -> bool:
        """
        Remove all occurrences of a shared label across all projects.

        This action removes the label string from all tasks where it appears.

        :param name: The name of the shared label to remove.
        :return: True if the removal was successful,
        :raises requests.exceptions.HTTPError: If the API request fails.
        """
        endpoint = get_api_url(SHARED_LABELS_REMOVE_PATH)
        data = {"name": name}
        return post(self._session, endpoint, self._token, data=data)

add_comment(content, *, project_id=None, task_id=None, attachment=None, uids_to_notify=None)

Create a new comment on a task or project.

Requires either project_id or task_id to be set, and can optionally include an attachment object.

Parameters:

Name Type Description Default
content str

The text content of the comment (supports Markdown).

required
project_id str | None

The ID of the project to add the comment to.

None
task_id str | None

The ID of the task to add the comment to.

None
attachment Attachment | None

The attachment object to include with the comment.

None
uids_to_notify list[str] | None

A list of user IDs to notify.

None

Returns:

Type Description
Comment

The newly created comment.

Raises:

Type Description
ValueError

If neither project_id nor task_id is provided.

requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response is not a valid Comment dictionary.

Source code in todoist_api_python/api.py
def add_comment(
    self,
    content: Annotated[str, MaxLen(15000)],
    *,
    project_id: str | None = None,
    task_id: str | None = None,
    attachment: Attachment | None = None,
    uids_to_notify: list[str] | None = None,
) -> Comment:
    """
    Create a new comment on a task or project.

    Requires either `project_id` or `task_id` to be set,
    and can optionally include an `attachment` object.

    :param content: The text content of the comment (supports Markdown).
    :param project_id: The ID of the project to add the comment to.
    :param task_id: The ID of the task to add the comment to.
    :param attachment: The attachment object to include with the comment.
    :param uids_to_notify: A list of user IDs to notify.
    :return: The newly created comment.
    :raises ValueError: If neither `project_id` nor `task_id` is provided.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response is not a valid Comment dictionary.
    """
    if project_id is None and task_id is None:
        raise ValueError("Either `project_id` or `task_id` must be provided.")

    endpoint = get_api_url(COMMENTS_PATH)

    data: dict[str, Any] = {"content": content}
    if project_id is not None:
        data["project_id"] = project_id
    if task_id is not None:
        data["task_id"] = task_id
    if attachment is not None:
        data["attachment"] = attachment.to_dict()
    if uids_to_notify is not None:
        data["uids_to_notify"] = uids_to_notify

    comment_data: dict[str, Any] = post(
        self._session, endpoint, self._token, data=data
    )
    return Comment.from_dict(comment_data)

add_label(name, *, color=None, item_order=None, is_favorite=None)

Create a new personal label.

Parameters:

Name Type Description Default
name str

The name of the label.

required
color ColorString | None

The color of the label icon.

None
item_order int | None

Label's order in the label list.

None
is_favorite bool | None

Whether the label is a favorite.

None

Returns:

Type Description
Label

The newly created label.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response is not a valid Label dictionary.

Source code in todoist_api_python/api.py
def add_label(
    self,
    name: Annotated[str, MinLen(1), MaxLen(60)],
    *,
    color: ColorString | None = None,
    item_order: int | None = None,
    is_favorite: bool | None = None,
) -> Label:
    """
    Create a new personal label.

    :param name: The name of the label.
    :param color: The color of the label icon.
    :param item_order: Label's order in the label list.
    :param is_favorite: Whether the label is a favorite.
    :return: The newly created label.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response is not a valid Label dictionary.
    """
    endpoint = get_api_url(LABELS_PATH)

    data: dict[str, Any] = {"name": name}

    if color is not None:
        data["color"] = color
    if item_order is not None:
        data["item_order"] = item_order
    if is_favorite is not None:
        data["is_favorite"] = is_favorite

    label_data: dict[str, Any] = post(
        self._session, endpoint, self._token, data=data
    )
    return Label.from_dict(label_data)

add_project(name, *, description=None, parent_id=None, color=None, is_favorite=None, view_style=None)

Create a new project.

Parameters:

Name Type Description Default
name str

The name of the project.

required
description str | None

Description for the project (up to 1024 characters).

None
parent_id str | None

The ID of the parent project. Set to null for root projects.

None
color ColorString | None

The color of the project icon.

None
is_favorite bool | None

Whether the project is a favorite.

None
view_style ViewStyle | None

A string value (either 'list' or 'board', default is 'list').

None

Returns:

Type Description
Project

The newly created project.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response is not a valid Project dictionary.

Source code in todoist_api_python/api.py
def add_project(
    self,
    name: Annotated[str, MinLen(1), MaxLen(120)],
    *,
    description: Annotated[str, MaxLen(16383)] | None = None,
    parent_id: str | None = None,
    color: ColorString | None = None,
    is_favorite: bool | None = None,
    view_style: ViewStyle | None = None,
) -> Project:
    """
    Create a new project.

    :param name: The name of the project.
    :param description: Description for the project (up to 1024 characters).
    :param parent_id: The ID of the parent project. Set to null for root projects.
    :param color: The color of the project icon.
    :param is_favorite: Whether the project is a favorite.
    :param view_style: A string value (either 'list' or 'board', default is 'list').
    :return: The newly created project.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response is not a valid Project dictionary.
    """
    endpoint = get_api_url(PROJECTS_PATH)

    data: dict[str, Any] = {"name": name}
    if parent_id is not None:
        data["parent_id"] = parent_id
    if description is not None:
        data["description"] = description
    if color is not None:
        data["color"] = color
    if is_favorite is not None:
        data["is_favorite"] = is_favorite
    if view_style is not None:
        data["view_style"] = view_style

    project_data: dict[str, Any] = post(
        self._session, endpoint, self._token, data=data
    )
    return Project.from_dict(project_data)

add_section(name, project_id, *, order=None)

Create a new section within a project.

Parameters:

Name Type Description Default
name str

The name of the section.

required
project_id str

The ID of the project to add the section to.

required
order int | None

The order of the section among all sections in the project.

None

Returns:

Type Description
Section

The newly created section.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response is not a valid Section dictionary.

Source code in todoist_api_python/api.py
def add_section(
    self,
    name: Annotated[str, MinLen(1), MaxLen(2048)],
    project_id: str,
    *,
    order: int | None = None,
) -> Section:
    """
    Create a new section within a project.

    :param name: The name of the section.
    :param project_id: The ID of the project to add the section to.
    :param order: The order of the section among all sections in the project.
    :return: The newly created section.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response is not a valid Section dictionary.
    """
    endpoint = get_api_url(SECTIONS_PATH)

    data: dict[str, Any] = {"name": name, "project_id": project_id}
    if order is not None:
        data["order"] = order

    section_data: dict[str, Any] = post(
        self._session, endpoint, self._token, data=data
    )
    return Section.from_dict(section_data)

add_task(content, *, description=None, project_id=None, section_id=None, parent_id=None, labels=None, priority=None, due_string=None, due_lang=None, due_date=None, due_datetime=None, assignee_id=None, order=None, auto_reminder=None, auto_parse_labels=None, duration=None, duration_unit=None, deadline_date=None, deadline_lang=None)

Create a new task.

Parameters:

Name Type Description Default
content str

The text content of the task.

required
project_id str | None

The ID of the project to add the task to.

None
section_id str | None

The ID of the section to add the task to.

None
parent_id str | None

The ID of the parent task.

None
labels list[str] | None

The task's labels (a list of names).

None
priority int | None

The priority of the task (4 for very urgent).

None
due_string str | None

The due date in natural language format.

None
due_lang LanguageCode | None

Language for parsing the due date (e.g., 'en').

None
due_date date | None

The due date as a date object.

None
due_datetime datetime | None

The due date and time as a datetime object.

None
assignee_id str | None

User ID to whom the task is assigned.

None
description str | None

Description for the task.

None
order int | None

The order of task in the project or section.

None
auto_reminder bool | None

Whether to add default reminder if date with time is set.

None
auto_parse_labels bool | None

Whether to parse labels from task content.

None
duration int | None

The amount of time the task will take.

None
duration_unit Literal['minute', 'day'] | None

The unit of time for duration.

None
deadline_date date | None

The deadline date as a date object.

None
deadline_lang LanguageCode | None

Language for parsing the deadline date.

None

Returns:

Type Description
Task

The newly created task.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response is not a valid Task dictionary.

Source code in todoist_api_python/api.py
def add_task(  # noqa: PLR0912
    self,
    content: Annotated[str, MinLen(1), MaxLen(500)],
    *,
    description: Annotated[str, MaxLen(16383)] | None = None,
    project_id: str | None = None,
    section_id: str | None = None,
    parent_id: str | None = None,
    labels: list[Annotated[str, MaxLen(100)]] | None = None,
    priority: Annotated[int, Ge(1), Le(4)] | None = None,
    due_string: Annotated[str, MaxLen(150)] | None = None,
    due_lang: LanguageCode | None = None,
    due_date: date | None = None,
    due_datetime: datetime | None = None,
    assignee_id: str | None = None,
    order: int | None = None,
    auto_reminder: bool | None = None,
    auto_parse_labels: bool | None = None,
    duration: Annotated[int, Ge(1)] | None = None,
    duration_unit: Literal["minute", "day"] | None = None,
    deadline_date: date | None = None,
    deadline_lang: LanguageCode | None = None,
) -> Task:
    """
    Create a new task.

    :param content: The text content of the task.
    :param project_id: The ID of the project to add the task to.
    :param section_id: The ID of the section to add the task to.
    :param parent_id: The ID of the parent task.
    :param labels: The task's labels (a list of names).
    :param priority: The priority of the task (4 for very urgent).
    :param due_string: The due date in natural language format.
    :param due_lang: Language for parsing the due date (e.g., 'en').
    :param due_date: The due date as a date object.
    :param due_datetime: The due date and time as a datetime object.
    :param assignee_id: User ID to whom the task is assigned.
    :param description: Description for the task.
    :param order: The order of task in the project or section.
    :param auto_reminder: Whether to add default reminder if date with time is set.
    :param auto_parse_labels: Whether to parse labels from task content.
    :param duration: The amount of time the task will take.
    :param duration_unit: The unit of time for duration.
    :param deadline_date: The deadline date as a date object.
    :param deadline_lang: Language for parsing the deadline date.
    :return: The newly created task.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response is not a valid Task dictionary.
    """
    endpoint = get_api_url(TASKS_PATH)

    data: dict[str, Any] = {"content": content}
    if description is not None:
        data["description"] = description
    if project_id is not None:
        data["project_id"] = project_id
    if section_id is not None:
        data["section_id"] = section_id
    if parent_id is not None:
        data["parent_id"] = parent_id
    if labels is not None:
        data["labels"] = labels
    if priority is not None:
        data["priority"] = priority
    if due_string is not None:
        data["due_string"] = due_string
    if due_lang is not None:
        data["due_lang"] = due_lang
    if due_date is not None:
        data["due_date"] = format_date(due_date)
    if due_datetime is not None:
        data["due_datetime"] = format_datetime(due_datetime)
    if assignee_id is not None:
        data["assignee_id"] = assignee_id
    if order is not None:
        data["order"] = order
    if auto_reminder is not None:
        data["auto_reminder"] = auto_reminder
    if auto_parse_labels is not None:
        data["auto_parse_labels"] = auto_parse_labels
    if duration is not None:
        data["duration"] = duration
    if duration_unit is not None:
        data["duration_unit"] = duration_unit
    if deadline_date is not None:
        data["deadline_date"] = format_date(deadline_date)
    if deadline_lang is not None:
        data["deadline_lang"] = deadline_lang

    task_data: dict[str, Any] = post(
        self._session, endpoint, self._token, data=data
    )
    return Task.from_dict(task_data)

add_task_quick(text, *, note=None, reminder=None, auto_reminder=True)

Create a new task using Todoist's Quick Add syntax.

This automatically parses dates, deadlines, projects, labels, priorities, etc, from the provided text (e.g., "Buy milk #Shopping @groceries tomorrow p1").

Parameters:

Name Type Description Default
text str

The task text using Quick Add syntax.

required
note str | None

Optional note to be added to the task.

None
reminder str | None

Optional reminder date in free form text.

None
auto_reminder bool

Whether to add default reminder if date with time is set.

True

Returns:

Type Description
Task

A result object containing the parsed task data and metadata.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response cannot be parsed into a QuickAddResult.

Source code in todoist_api_python/api.py
def add_task_quick(
    self,
    text: str,
    *,
    note: str | None = None,
    reminder: str | None = None,
    auto_reminder: bool = True,
) -> Task:
    """
    Create a new task using Todoist's Quick Add syntax.

    This automatically parses dates, deadlines, projects, labels, priorities, etc,
    from the provided text (e.g., "Buy milk #Shopping @groceries tomorrow p1").

    :param text: The task text using Quick Add syntax.
    :param note: Optional note to be added to the task.
    :param reminder: Optional reminder date in free form text.
    :param auto_reminder: Whether to add default reminder if date with time is set.
    :return: A result object containing the parsed task data and metadata.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response cannot be parsed into a QuickAddResult.
    """
    endpoint = get_api_url(TASKS_QUICK_ADD_PATH)

    data = {
        "meta": True,
        "text": text,
        "auto_reminder": auto_reminder,
    }

    if note is not None:
        data["note"] = note
    if reminder is not None:
        data["reminder"] = reminder

    task_data: dict[str, Any] = post(
        self._session, endpoint, self._token, data=data
    )
    return Task.from_dict(task_data)

archive_project(project_id)

Archive a project.

For personal projects, archives it only for the user. For workspace projects, archives it for all members.

Parameters:

Name Type Description Default
project_id str

The ID of the project to archive.

required

Returns:

Type Description
Project

The archived project object.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response is not a valid Project dictionary.

Source code in todoist_api_python/api.py
def archive_project(self, project_id: str) -> Project:
    """
    Archive a project.

    For personal projects, archives it only for the user.
    For workspace projects, archives it for all members.

    :param project_id: The ID of the project to archive.
    :return: The archived project object.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response is not a valid Project dictionary.
    """
    endpoint = get_api_url(
        f"{PROJECTS_PATH}/{project_id}/{PROJECT_ARCHIVE_PATH_SUFFIX}"
    )
    project_data: dict[str, Any] = post(self._session, endpoint, self._token)
    return Project.from_dict(project_data)

complete_task(task_id)

Complete a task.

For recurring tasks, this schedules the next occurrence. For non-recurring tasks, it marks them as completed.

Parameters:

Name Type Description Default
task_id str

The ID of the task to close.

required

Returns:

Type Description
bool

True if the task was closed successfully, False otherwise (possibly raise HTTPError instead).

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

Source code in todoist_api_python/api.py
def complete_task(self, task_id: str) -> bool:
    """
    Complete a task.

    For recurring tasks, this schedules the next occurrence.
    For non-recurring tasks, it marks them as completed.

    :param task_id: The ID of the task to close.
    :return: True if the task was closed successfully,
             False otherwise (possibly raise `HTTPError` instead).
    :raises requests.exceptions.HTTPError: If the API request fails.
    """
    endpoint = get_api_url(f"{TASKS_PATH}/{task_id}/close")
    return post(self._session, endpoint, self._token)

delete_comment(comment_id)

Delete a comment.

Parameters:

Name Type Description Default
comment_id str

The ID of the comment to delete.

required

Returns:

Type Description
bool

True if the comment was deleted successfully, False otherwise (possibly raise HTTPError instead).

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

Source code in todoist_api_python/api.py
def delete_comment(self, comment_id: str) -> bool:
    """
    Delete a comment.

    :param comment_id: The ID of the comment to delete.
    :return: True if the comment was deleted successfully,
             False otherwise (possibly raise `HTTPError` instead).
    :raises requests.exceptions.HTTPError: If the API request fails.
    """
    endpoint = get_api_url(f"{COMMENTS_PATH}/{comment_id}")
    return delete(self._session, endpoint, self._token)

delete_label(label_id)

Delete a personal label.

Instances of the label will be removed from tasks.

Parameters:

Name Type Description Default
label_id str

The ID of the label to delete.

required

Returns:

Type Description
bool

True if the label was deleted successfully, False otherwise (possibly raise HTTPError instead).

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

Source code in todoist_api_python/api.py
def delete_label(self, label_id: str) -> bool:
    """
    Delete a personal label.

    Instances of the label will be removed from tasks.

    :param label_id: The ID of the label to delete.
    :return: True if the label was deleted successfully,
             False otherwise (possibly raise `HTTPError` instead).
    :raises requests.exceptions.HTTPError: If the API request fails.
    """
    endpoint = get_api_url(f"{LABELS_PATH}/{label_id}")
    return delete(self._session, endpoint, self._token)

delete_project(project_id)

Delete a project.

All nested sections and tasks will also be deleted.

Parameters:

Name Type Description Default
project_id str

The ID of the project to delete.

required

Returns:

Type Description
bool

True if the project was deleted successfully, False otherwise (possibly raise HTTPError instead).

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

Source code in todoist_api_python/api.py
def delete_project(self, project_id: str) -> bool:
    """
    Delete a project.

    All nested sections and tasks will also be deleted.

    :param project_id: The ID of the project to delete.
    :return: True if the project was deleted successfully,
             False otherwise (possibly raise `HTTPError` instead).
    :raises requests.exceptions.HTTPError: If the API request fails.
    """
    endpoint = get_api_url(f"{PROJECTS_PATH}/{project_id}")
    return delete(self._session, endpoint, self._token)

delete_section(section_id)

Delete a section.

All tasks within the section will also be deleted.

Parameters:

Name Type Description Default
section_id str

The ID of the section to delete.

required

Returns:

Type Description
bool

True if the section was deleted successfully, False otherwise (possibly raise HTTPError instead).

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

Source code in todoist_api_python/api.py
def delete_section(self, section_id: str) -> bool:
    """
    Delete a section.

    All tasks within the section will also be deleted.

    :param section_id: The ID of the section to delete.
    :return: True if the section was deleted successfully,
             False otherwise (possibly raise `HTTPError` instead).
    :raises requests.exceptions.HTTPError: If the API request fails.
    """
    endpoint = get_api_url(f"{SECTIONS_PATH}/{section_id}")
    return delete(self._session, endpoint, self._token)

delete_task(task_id)

Delete a task.

Parameters:

Name Type Description Default
task_id str

The ID of the task to delete.

required

Returns:

Type Description
bool

True if the task was deleted successfully, False otherwise (possibly raise HTTPError instead).

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

Source code in todoist_api_python/api.py
def delete_task(self, task_id: str) -> bool:
    """
    Delete a task.

    :param task_id: The ID of the task to delete.
    :return: True if the task was deleted successfully,
             False otherwise (possibly raise `HTTPError` instead).
    :raises requests.exceptions.HTTPError: If the API request fails.
    """
    endpoint = get_api_url(f"{TASKS_PATH}/{task_id}")
    return delete(self._session, endpoint, self._token)

filter_tasks(*, query=None, lang=None, limit=None)

Get an iterable of lists of active tasks matching the filter.

The response is an iterable of lists of active tasks matching the criteria. Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions.

Parameters:

Name Type Description Default
query str | None

Query tasks using Todoist's filter language.

None
lang str | None

Language for task content (e.g., 'en').

None
limit int | None

Maximum number of tasks per page.

None

Returns:

Type Description
Iterator[list[Task]]

An iterable of lists of tasks.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response structure is unexpected.

Source code in todoist_api_python/api.py
def filter_tasks(
    self,
    *,
    query: Annotated[str, MaxLen(1024)] | None = None,
    lang: str | None = None,
    limit: Annotated[int, Ge(1), Le(200)] | None = None,
) -> Iterator[list[Task]]:
    """
    Get an iterable of lists of active tasks matching the filter.

    The response is an iterable of lists of active tasks matching the criteria.
    Be aware that each iteration fires off a network request to the Todoist API,
    and may result in rate limiting or other API restrictions.

    :param query: Query tasks using Todoist's filter language.
    :param lang: Language for task content (e.g., 'en').
    :param limit: Maximum number of tasks per page.
    :return: An iterable of lists of tasks.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response structure is unexpected.
    """
    endpoint = get_api_url(TASKS_FILTER_PATH)

    params: dict[str, Any] = {}
    if query is not None:
        params["query"] = query
    if lang is not None:
        params["lang"] = lang
    if limit is not None:
        params["limit"] = limit

    return ResultsPaginator(
        self._session,
        endpoint,
        "results",
        Task.from_dict,
        self._token,
        params,
    )

get_collaborators(project_id, limit=None)

Get an iterable of lists of collaborators in shared projects.

The response is an iterable of lists of collaborators in shared projects, Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions.

Parameters:

Name Type Description Default
project_id str

The ID of the project.

required
limit int | None

Maximum number of collaborators per page.

None

Returns:

Type Description
Iterator[list[Collaborator]]

An iterable of lists of collaborators.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response structure is unexpected.

Source code in todoist_api_python/api.py
def get_collaborators(
    self,
    project_id: str,
    limit: Annotated[int, Ge(1), Le(200)] | None = None,
) -> Iterator[list[Collaborator]]:
    """
    Get an iterable of lists of collaborators in shared projects.

    The response is an iterable of lists of collaborators in shared projects,
    Be aware that each iteration fires off a network request to the Todoist API,
    and may result in rate limiting or other API restrictions.

    :param project_id: The ID of the project.
    :param limit: Maximum number of collaborators per page.
    :return: An iterable of lists of collaborators.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response structure is unexpected.
    """
    endpoint = get_api_url(f"{PROJECTS_PATH}/{project_id}/{COLLABORATORS_PATH}")
    params: dict[str, Any] = {}
    if limit is not None:
        params["limit"] = limit
    return ResultsPaginator(
        self._session,
        endpoint,
        "results",
        Collaborator.from_dict,
        self._token,
        params,
    )

get_comment(comment_id)

Get a specific comment by its ID.

Parameters:

Name Type Description Default
comment_id str

The ID of the comment to retrieve.

required

Returns:

Type Description
Comment

The requested comment.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response is not a valid Comment dictionary.

Source code in todoist_api_python/api.py
def get_comment(self, comment_id: str) -> Comment:
    """
    Get a specific comment by its ID.

    :param comment_id: The ID of the comment to retrieve.
    :return: The requested comment.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response is not a valid Comment dictionary.
    """
    endpoint = get_api_url(f"{COMMENTS_PATH}/{comment_id}")
    comment_data: dict[str, Any] = get(self._session, endpoint, self._token)
    return Comment.from_dict(comment_data)

get_comments(*, project_id=None, task_id=None, limit=None)

Get an iterable of lists of comments for a task or project.

Requires either project_id or task_id to be set.

The response is an iterable of lists of comments. Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions.

Parameters:

Name Type Description Default
project_id str | None

The ID of the project to retrieve comments for.

None
task_id str | None

The ID of the task to retrieve comments for.

None
limit int | None

Maximum number of comments per page.

None

Returns:

Type Description
Iterator[list[Comment]]

An iterable of lists of comments.

Raises:

Type Description
ValueError

If neither project_id nor task_id is provided.

requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response structure is unexpected.

Source code in todoist_api_python/api.py
def get_comments(
    self,
    *,
    project_id: str | None = None,
    task_id: str | None = None,
    limit: Annotated[int, Ge(1), Le(200)] | None = None,
) -> Iterator[list[Comment]]:
    """
    Get an iterable of lists of comments for a task or project.

    Requires either `project_id` or `task_id` to be set.

    The response is an iterable of lists of comments.
    Be aware that each iteration fires off a network request to the Todoist API,
    and may result in rate limiting or other API restrictions.

    :param project_id: The ID of the project to retrieve comments for.
    :param task_id: The ID of the task to retrieve comments for.
    :param limit: Maximum number of comments per page.
    :return: An iterable of lists of comments.
    :raises ValueError: If neither `project_id` nor `task_id` is provided.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response structure is unexpected.
    """
    if project_id is None and task_id is None:
        raise ValueError("Either `project_id` or `task_id` must be provided.")

    endpoint = get_api_url(COMMENTS_PATH)

    params: dict[str, Any] = {}
    if project_id is not None:
        params["project_id"] = project_id
    if task_id is not None:
        params["task_id"] = task_id
    if limit is not None:
        params["limit"] = limit

    return ResultsPaginator(
        self._session, endpoint, "results", Comment.from_dict, self._token, params
    )

get_completed_tasks_by_completion_date(*, since, until, workspace_id=None, filter_query=None, filter_lang=None, limit=None)

Get an iterable of lists of completed tasks within a date range.

Retrieves tasks completed within a specific date range (up to 3 months). Supports filtering by workspace or a filter query.

The response is an iterable of lists of completed tasks. Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions.

Parameters:

Name Type Description Default
since datetime

Start of the date range (inclusive).

required
until datetime

End of the date range (inclusive).

required
workspace_id str | None

Filter by workspace ID.

None
filter_query str | None

Filter by a query string.

None
filter_lang str | None

Language for the filter query (e.g., 'en').

None
limit int | None

Maximum number of tasks per page (default 50).

None

Returns:

Type Description
Iterator[list[Task]]

An iterable of lists of completed tasks.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response structure is unexpected.

Source code in todoist_api_python/api.py
def get_completed_tasks_by_completion_date(
    self,
    *,
    since: datetime,
    until: datetime,
    workspace_id: str | None = None,
    filter_query: str | None = None,
    filter_lang: str | None = None,
    limit: Annotated[int, Ge(1), Le(200)] | None = None,
) -> Iterator[list[Task]]:
    """
    Get an iterable of lists of completed tasks within a date range.

    Retrieves tasks completed within a specific date range (up to 3 months).
    Supports filtering by workspace or a filter query.

    The response is an iterable of lists of completed tasks. Be aware that each
    iteration fires off a network request to the Todoist API, and may result in
    rate limiting or other API restrictions.

    :param since: Start of the date range (inclusive).
    :param until: End of the date range (inclusive).
    :param workspace_id: Filter by workspace ID.
    :param filter_query: Filter by a query string.
    :param filter_lang: Language for the filter query (e.g., 'en').
    :param limit: Maximum number of tasks per page (default 50).
    :return: An iterable of lists of completed tasks.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response structure is unexpected.
    """
    endpoint = get_api_url(TASKS_COMPLETED_BY_COMPLETION_DATE_PATH)

    params: dict[str, Any] = {
        "since": format_datetime(since),
        "until": format_datetime(until),
    }
    if workspace_id is not None:
        params["workspace_id"] = workspace_id
    if filter_query is not None:
        params["filter_query"] = filter_query
    if filter_lang is not None:
        params["filter_lang"] = filter_lang
    if limit is not None:
        params["limit"] = limit

    return ResultsPaginator(
        self._session, endpoint, "items", Task.from_dict, self._token, params
    )

get_completed_tasks_by_due_date(*, since, until, workspace_id=None, project_id=None, section_id=None, parent_id=None, filter_query=None, filter_lang=None, limit=None)

Get an iterable of lists of completed tasks within a due date range.

Retrieves tasks completed within a specific due date range (up to 6 weeks). Supports filtering by workspace, project, section, parent task, or a query.

The response is an iterable of lists of completed tasks. Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions.

Parameters:

Name Type Description Default
since datetime

Start of the date range (inclusive).

required
until datetime

End of the date range (inclusive).

required
workspace_id str | None

Filter by workspace ID.

None
project_id str | None

Filter by project ID.

None
section_id str | None

Filter by section ID.

None
parent_id str | None

Filter by parent task ID.

None
filter_query str | None

Filter by a query string.

None
filter_lang str | None

Language for the filter query (e.g., 'en').

None
limit int | None

Maximum number of tasks per page (default 50).

None

Returns:

Type Description
Iterator[list[Task]]

An iterable of lists of completed tasks.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response structure is unexpected.

Source code in todoist_api_python/api.py
def get_completed_tasks_by_due_date(
    self,
    *,
    since: datetime,
    until: datetime,
    workspace_id: str | None = None,
    project_id: str | None = None,
    section_id: str | None = None,
    parent_id: str | None = None,
    filter_query: str | None = None,
    filter_lang: str | None = None,
    limit: Annotated[int, Ge(1), Le(200)] | None = None,
) -> Iterator[list[Task]]:
    """
    Get an iterable of lists of completed tasks within a due date range.

    Retrieves tasks completed within a specific due date range (up to 6 weeks).
    Supports filtering by workspace, project, section, parent task, or a query.

    The response is an iterable of lists of completed tasks. Be aware that each
    iteration fires off a network request to the Todoist API, and may result in
    rate limiting or other API restrictions.

    :param since: Start of the date range (inclusive).
    :param until: End of the date range (inclusive).
    :param workspace_id: Filter by workspace ID.
    :param project_id: Filter by project ID.
    :param section_id: Filter by section ID.
    :param parent_id: Filter by parent task ID.
    :param filter_query: Filter by a query string.
    :param filter_lang: Language for the filter query (e.g., 'en').
    :param limit: Maximum number of tasks per page (default 50).
    :return: An iterable of lists of completed tasks.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response structure is unexpected.
    """
    endpoint = get_api_url(TASKS_COMPLETED_BY_DUE_DATE_PATH)

    params: dict[str, Any] = {
        "since": format_datetime(since),
        "until": format_datetime(until),
    }
    if workspace_id is not None:
        params["workspace_id"] = workspace_id
    if project_id is not None:
        params["project_id"] = project_id
    if section_id is not None:
        params["section_id"] = section_id
    if parent_id is not None:
        params["parent_id"] = parent_id
    if filter_query is not None:
        params["filter_query"] = filter_query
    if filter_lang is not None:
        params["filter_lang"] = filter_lang
    if limit is not None:
        params["limit"] = limit

    return ResultsPaginator(
        self._session, endpoint, "items", Task.from_dict, self._token, params
    )

get_label(label_id)

Get a specific personal label by its ID.

Parameters:

Name Type Description Default
label_id str

The ID of the label to retrieve.

required

Returns:

Type Description
Label

The requested label.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response is not a valid Label dictionary.

Source code in todoist_api_python/api.py
def get_label(self, label_id: str) -> Label:
    """
    Get a specific personal label by its ID.

    :param label_id: The ID of the label to retrieve.
    :return: The requested label.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response is not a valid Label dictionary.
    """
    endpoint = get_api_url(f"{LABELS_PATH}/{label_id}")
    label_data: dict[str, Any] = get(self._session, endpoint, self._token)
    return Label.from_dict(label_data)

get_labels(*, limit=None)

Get an iterable of lists of personal labels.

Supports pagination arguments.

The response is an iterable of lists of personal labels. Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions.

Parameters:

Name Type Description Default
limit int | None

` number of labels per page.

None

Returns:

Type Description
Iterator[list[Label]]

An iterable of lists of personal labels.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response structure is unexpected.

Source code in todoist_api_python/api.py
def get_labels(
    self,
    *,
    limit: Annotated[int, Ge(1), Le(200)] | None = None,
) -> Iterator[list[Label]]:
    """
    Get an iterable of lists of personal labels.

    Supports pagination arguments.

    The response is an iterable of lists of personal labels.
    Be aware that each iteration fires off a network request to the Todoist API,
    and may result in rate limiting or other API restrictions.

    :param limit: ` number of labels per page.
    :return: An iterable of lists of personal labels.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response structure is unexpected.
    """
    endpoint = get_api_url(LABELS_PATH)

    params: dict[str, Any] = {}
    if limit is not None:
        params["limit"] = limit

    return ResultsPaginator(
        self._session, endpoint, "results", Label.from_dict, self._token, params
    )

get_project(project_id)

Get a project by its ID.

Parameters:

Name Type Description Default
project_id str

The ID of the project to retrieve.

required

Returns:

Type Description
Project

The requested project.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response is not a valid Project dictionary.

Source code in todoist_api_python/api.py
def get_project(self, project_id: str) -> Project:
    """
    Get a project by its ID.

    :param project_id: The ID of the project to retrieve.
    :return: The requested project.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response is not a valid Project dictionary.
    """
    endpoint = get_api_url(f"{PROJECTS_PATH}/{project_id}")
    project_data: dict[str, Any] = get(self._session, endpoint, self._token)
    return Project.from_dict(project_data)

get_projects(limit=None)

Get an iterable of lists of active projects.

The response is an iterable of lists of active projects. Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions.

Parameters:

Name Type Description Default
limit int | None

Maximum number of projects per page.

None

Returns:

Type Description
Iterator[list[Project]]

An iterable of lists of projects.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response structure is unexpected.

Source code in todoist_api_python/api.py
def get_projects(
    self,
    limit: Annotated[int, Ge(1), Le(200)] | None = None,
) -> Iterator[list[Project]]:
    """
    Get an iterable of lists of active projects.

    The response is an iterable of lists of active projects.
    Be aware that each iteration fires off a network request to the Todoist API,
    and may result in rate limiting or other API restrictions.

    :param limit: Maximum number of projects per page.
    :return: An iterable of lists of projects.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response structure is unexpected.
    """
    endpoint = get_api_url(PROJECTS_PATH)
    params: dict[str, Any] = {}
    if limit is not None:
        params["limit"] = limit
    return ResultsPaginator(
        self._session, endpoint, "results", Project.from_dict, self._token, params
    )

get_section(section_id)

Get a specific section by its ID.

Parameters:

Name Type Description Default
section_id str

The ID of the section to retrieve.

required

Returns:

Type Description
Section

The requested section.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response is not a valid Section dictionary.

Source code in todoist_api_python/api.py
def get_section(self, section_id: str) -> Section:
    """
    Get a specific section by its ID.

    :param section_id: The ID of the section to retrieve.
    :return: The requested section.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response is not a valid Section dictionary.
    """
    endpoint = get_api_url(f"{SECTIONS_PATH}/{section_id}")
    section_data: dict[str, Any] = get(self._session, endpoint, self._token)
    return Section.from_dict(section_data)

get_sections(project_id=None, *, limit=None)

Get an iterable of lists of active sections.

Supports filtering by project_id and pagination arguments.

The response is an iterable of lists of active sections. Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions.

Parameters:

Name Type Description Default
project_id str | None

Filter sections by project ID.

None
limit int | None

Maximum number of sections per page.

None

Returns:

Type Description
Iterator[list[Section]]

An iterable of lists of sections.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response structure is unexpected.

Source code in todoist_api_python/api.py
def get_sections(
    self,
    project_id: str | None = None,
    *,
    limit: Annotated[int, Ge(1), Le(200)] | None = None,
) -> Iterator[list[Section]]:
    """
    Get an iterable of lists of active sections.

    Supports filtering by `project_id` and pagination arguments.

    The response is an iterable of lists of active sections.
    Be aware that each iteration fires off a network request to the Todoist API,
    and may result in rate limiting or other API restrictions.

    :param project_id: Filter sections by project ID.
    :param limit: Maximum number of sections per page.
    :return: An iterable of lists of sections.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response structure is unexpected.
    """
    endpoint = get_api_url(SECTIONS_PATH)

    params: dict[str, Any] = {}
    if project_id is not None:
        params["project_id"] = project_id
    if limit is not None:
        params["limit"] = limit

    return ResultsPaginator(
        self._session, endpoint, "results", Section.from_dict, self._token, params
    )

get_shared_labels(*, omit_personal=False, limit=None)

Get an iterable of lists of shared label names.

Includes labels from collaborators on shared projects that are not in the user's personal labels. Can optionally exclude personal label names using omit_personal=True. Supports pagination arguments.

The response is an iterable of lists of shared label names. Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions.

Parameters:

Name Type Description Default
omit_personal bool

Optional boolean flag to omit personal label names.

False
limit int | None

Maximum number of labels per page.

None

Returns:

Type Description
Iterator[list[str]]

An iterable of lists of shared label names (strings).

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response structure is unexpected.

Source code in todoist_api_python/api.py
def get_shared_labels(
    self,
    *,
    omit_personal: bool = False,
    limit: Annotated[int, Ge(1), Le(200)] | None = None,
) -> Iterator[list[str]]:
    """
    Get an iterable of lists of shared label names.

    Includes labels from collaborators on shared projects that are not in the
    user's personal labels. Can optionally exclude personal label names using
    `omit_personal=True`. Supports pagination arguments.

    The response is an iterable of lists of shared label names.
    Be aware that each iteration fires off a network request to the Todoist API,
    and may result in rate limiting or other API restrictions.

    :param omit_personal: Optional boolean flag to omit personal label names.
    :param limit: Maximum number of labels per page.
    :return: An iterable of lists of shared label names (strings).
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response structure is unexpected.
    """
    endpoint = get_api_url(SHARED_LABELS_PATH)

    params: dict[str, Any] = {"omit_personal": omit_personal}
    if limit is not None:
        params["limit"] = limit

    return ResultsPaginator(
        self._session, endpoint, "results", str, self._token, params
    )

get_task(task_id)

Get a specific task by its ID.

Parameters:

Name Type Description Default
task_id str

The ID of the task to retrieve.

required

Returns:

Type Description
Task

The requested task.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response is not a valid Task dictionary.

Source code in todoist_api_python/api.py
def get_task(self, task_id: str) -> Task:
    """
    Get a specific task by its ID.

    :param task_id: The ID of the task to retrieve.
    :return: The requested task.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response is not a valid Task dictionary.
    """
    endpoint = get_api_url(f"{TASKS_PATH}/{task_id}")
    task_data: dict[str, Any] = get(self._session, endpoint, self._token)
    return Task.from_dict(task_data)

get_tasks(*, project_id=None, section_id=None, parent_id=None, label=None, ids=None, limit=None)

Get an iterable of lists of active tasks.

The response is an iterable of lists of active tasks matching the criteria. Be aware that each iteration fires off a network request to the Todoist API, and may result in rate limiting or other API restrictions.

Parameters:

Name Type Description Default
project_id str | None

Filter tasks by project ID.

None
section_id str | None

Filter tasks by section ID.

None
parent_id str | None

Filter tasks by parent task ID.

None
label str | None

Filter tasks by label name.

None
ids list[str] | None

A list of the IDs of the tasks to retrieve.

None
limit int | None

Maximum number of tasks per page.

None

Returns:

Type Description
Iterator[list[Task]]

An iterable of lists of tasks.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response structure is unexpected.

Source code in todoist_api_python/api.py
def get_tasks(
    self,
    *,
    project_id: str | None = None,
    section_id: str | None = None,
    parent_id: str | None = None,
    label: str | None = None,
    ids: list[str] | None = None,
    limit: Annotated[int, Ge(1), Le(200)] | None = None,
) -> Iterator[list[Task]]:
    """
    Get an iterable of lists of active tasks.

    The response is an iterable of lists of active tasks matching the criteria.
    Be aware that each iteration fires off a network request to the Todoist API,
    and may result in rate limiting or other API restrictions.

    :param project_id: Filter tasks by project ID.
    :param section_id: Filter tasks by section ID.
    :param parent_id: Filter tasks by parent task ID.
    :param label: Filter tasks by label name.
    :param ids: A list of the IDs of the tasks to retrieve.
    :param limit: Maximum number of tasks per page.
    :return: An iterable of lists of tasks.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response structure is unexpected.
    """
    endpoint = get_api_url(TASKS_PATH)

    params: dict[str, Any] = {}
    if project_id is not None:
        params["project_id"] = project_id
    if section_id is not None:
        params["section_id"] = section_id
    if parent_id is not None:
        params["parent_id"] = parent_id
    if label is not None:
        params["label"] = label
    if ids is not None:
        params["ids"] = ",".join(str(i) for i in ids)
    if limit is not None:
        params["limit"] = limit

    return ResultsPaginator(
        self._session,
        endpoint,
        "results",
        Task.from_dict,
        self._token,
        params,
    )

remove_shared_label(name)

Remove all occurrences of a shared label across all projects.

This action removes the label string from all tasks where it appears.

Parameters:

Name Type Description Default
name str

The name of the shared label to remove.

required

Returns:

Type Description
bool

True if the removal was successful,

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

Source code in todoist_api_python/api.py
def remove_shared_label(self, name: Annotated[str, MaxLen(60)]) -> bool:
    """
    Remove all occurrences of a shared label across all projects.

    This action removes the label string from all tasks where it appears.

    :param name: The name of the shared label to remove.
    :return: True if the removal was successful,
    :raises requests.exceptions.HTTPError: If the API request fails.
    """
    endpoint = get_api_url(SHARED_LABELS_REMOVE_PATH)
    data = {"name": name}
    return post(self._session, endpoint, self._token, data=data)

rename_shared_label(name, new_name)

Rename all occurrences of a shared label across all projects.

Parameters:

Name Type Description Default
name str

The current name of the shared label to rename.

required
new_name str

The new name for the shared label.

required

Returns:

Type Description
bool

True if the rename was successful, False otherwise (possibly raise HTTPError instead).

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

Source code in todoist_api_python/api.py
def rename_shared_label(
    self,
    name: Annotated[str, MaxLen(60)],
    new_name: Annotated[str, MinLen(1), MaxLen(60)],
) -> bool:
    """
    Rename all occurrences of a shared label across all projects.

    :param name: The current name of the shared label to rename.
    :param new_name: The new name for the shared label.
    :return: True if the rename was successful,
             False otherwise (possibly raise `HTTPError` instead).
    :raises requests.exceptions.HTTPError: If the API request fails.
    """
    endpoint = get_api_url(SHARED_LABELS_RENAME_PATH)
    return post(
        self._session,
        endpoint,
        self._token,
        params={"name": name},
        data={"new_name": new_name},
    )

unarchive_project(project_id)

Unarchive a project.

Restores a previously archived project.

Parameters:

Name Type Description Default
project_id str

The ID of the project to unarchive.

required

Returns:

Type Description
Project

The unarchived project object.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

TypeError

If the API response is not a valid Project dictionary.

Source code in todoist_api_python/api.py
def unarchive_project(self, project_id: str) -> Project:
    """
    Unarchive a project.

    Restores a previously archived project.

    :param project_id: The ID of the project to unarchive.
    :return: The unarchived project object.
    :raises requests.exceptions.HTTPError: If the API request fails.
    :raises TypeError: If the API response is not a valid Project dictionary.
    """
    endpoint = get_api_url(
        f"{PROJECTS_PATH}/{project_id}/{PROJECT_UNARCHIVE_PATH_SUFFIX}"
    )
    project_data: dict[str, Any] = post(self._session, endpoint, self._token)
    return Project.from_dict(project_data)

uncomplete_task(task_id)

Uncomplete a (completed) task.

Any parent tasks or sections will also be uncompleted.

Parameters:

Name Type Description Default
task_id str

The ID of the task to reopen.

required

Returns:

Type Description
bool

True if the task was uncompleted successfully, False otherwise (possibly raise HTTPError instead).

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

Source code in todoist_api_python/api.py
def uncomplete_task(self, task_id: str) -> bool:
    """
    Uncomplete a (completed) task.

    Any parent tasks or sections will also be uncompleted.

    :param task_id: The ID of the task to reopen.
    :return: True if the task was uncompleted successfully,
             False otherwise (possibly raise `HTTPError` instead).
    :rtype: bool
    :raises requests.exceptions.HTTPError: If the API request fails.
    """
    endpoint = get_api_url(f"{TASKS_PATH}/{task_id}/reopen")
    return post(self._session, endpoint, self._token)

update_comment(comment_id, content)

Update an existing comment.

Currently, only content can be updated.

Parameters:

Name Type Description Default
comment_id str

The ID of the comment to update.

required
content str

The new text content for the comment.

required

Returns:

Type Description
Comment

the updated Comment.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

Source code in todoist_api_python/api.py
def update_comment(
    self, comment_id: str, content: Annotated[str, MaxLen(15000)]
) -> Comment:
    """
    Update an existing comment.

    Currently, only `content` can be updated.

    :param comment_id: The ID of the comment to update.
    :param content: The new text content for the comment.
    :return: the updated Comment.
    :raises requests.exceptions.HTTPError: If the API request fails.
    """
    endpoint = get_api_url(f"{COMMENTS_PATH}/{comment_id}")
    comment_data: dict[str, Any] = post(
        self._session, endpoint, self._token, data={"content": content}
    )
    return Comment.from_dict(comment_data)

update_label(label_id, *, name=None, color=None, item_order=None, is_favorite=None)

Update a personal label.

Only the fields to be updated need to be provided as keyword arguments.

Parameters:

Name Type Description Default
label_id str

The ID of the label.

required
name str | None

The name of the label.

None
color ColorString | None

The color of the label icon.

None
item_order int | None

Label's order in the label list.

None
is_favorite bool | None

Whether the label is a favorite.

None

Returns:

Type Description
Label

the updated Label.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

Source code in todoist_api_python/api.py
def update_label(
    self,
    label_id: str,
    *,
    name: Annotated[str, MinLen(1), MaxLen(60)] | None = None,
    color: ColorString | None = None,
    item_order: int | None = None,
    is_favorite: bool | None = None,
) -> Label:
    """
    Update a personal label.

    Only the fields to be updated need to be provided as keyword arguments.

    :param label_id: The ID of the label.
    :param name: The name of the label.
    :param color: The color of the label icon.
    :param item_order: Label's order in the label list.
    :param is_favorite: Whether the label is a favorite.
    :return: the updated Label.
    :raises requests.exceptions.HTTPError: If the API request fails.
    """
    endpoint = get_api_url(f"{LABELS_PATH}/{label_id}")

    data: dict[str, Any] = {}
    if name is not None:
        data["name"] = name
    if color is not None:
        data["color"] = color
    if item_order is not None:
        data["item_order"] = item_order
    if is_favorite is not None:
        data["is_favorite"] = is_favorite

    label_data: dict[str, Any] = post(
        self._session, endpoint, self._token, data=data
    )
    return Label.from_dict(label_data)

update_project(project_id, *, name=None, description=None, color=None, is_favorite=None, view_style=None)

Update an existing project.

Only the fields to be updated need to be provided as keyword arguments.

Parameters:

Name Type Description Default
project_id str

The ID of the project to update.

required
name str | None

The name of the project.

None
description str | None

Description for the project (up to 1024 characters).

None
color ColorString | None

The color of the project icon.

None
is_favorite bool | None

Whether the project is a favorite.

None
view_style ViewStyle | None

A string value (either 'list' or 'board').

None

Returns:

Type Description
Project

the updated Project.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

Source code in todoist_api_python/api.py
def update_project(
    self,
    project_id: str,
    *,
    name: Annotated[str, MinLen(1), MaxLen(120)] | None = None,
    description: Annotated[str, MaxLen(16383)] | None = None,
    color: ColorString | None = None,
    is_favorite: bool | None = None,
    view_style: ViewStyle | None = None,
) -> Project:
    """
    Update an existing project.

    Only the fields to be updated need to be provided as keyword arguments.

    :param project_id: The ID of the project to update.
    :param name: The name of the project.
    :param description: Description for the project (up to 1024 characters).
    :param color: The color of the project icon.
    :param is_favorite: Whether the project is a favorite.
    :param view_style: A string value (either 'list' or 'board').
    :return: the updated Project.
    :raises requests.exceptions.HTTPError: If the API request fails.
    """
    endpoint = get_api_url(f"{PROJECTS_PATH}/{project_id}")

    data: dict[str, Any] = {}

    if name is not None:
        data["name"] = name
    if description is not None:
        data["description"] = description
    if color is not None:
        data["color"] = color
    if is_favorite is not None:
        data["is_favorite"] = is_favorite
    if view_style is not None:
        data["view_style"] = view_style

    project_data: dict[str, Any] = post(
        self._session, endpoint, self._token, data=data
    )
    return Project.from_dict(project_data)

update_section(section_id, name)

Update an existing section.

Currently, only name can be updated.

Parameters:

Name Type Description Default
section_id str

The ID of the section to update.

required
name str

The new name for the section.

required

Returns:

Type Description
Section

the updated Section.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

Source code in todoist_api_python/api.py
def update_section(
    self,
    section_id: str,
    name: Annotated[str, MinLen(1), MaxLen(2048)],
) -> Section:
    """
    Update an existing section.

    Currently, only `name` can be updated.

    :param section_id: The ID of the section to update.
    :param name: The new name for the section.
    :return: the updated Section.
    :raises requests.exceptions.HTTPError: If the API request fails.
    """
    endpoint = get_api_url(f"{SECTIONS_PATH}/{section_id}")
    section_data: dict[str, Any] = post(
        self._session, endpoint, self._token, data={"name": name}
    )
    return Section.from_dict(section_data)

update_task(task_id, *, content=None, description=None, labels=None, priority=None, due_string=None, due_lang=None, due_date=None, due_datetime=None, assignee_id=None, day_order=None, collapsed=None, duration=None, duration_unit=None, deadline_date=None, deadline_lang=None)

Update an existing task.

Only the fields to be updated need to be provided.

Parameters:

Name Type Description Default
task_id str

The ID of the task to update.

required
content str | None

The text content of the task.

None
description str | None

Description for the task.

None
labels list[str] | None

The task's labels (a list of names).

None
priority int | None

The priority of the task (4 for very urgent).

None
due_string str | None

The due date in natural language format.

None
due_lang LanguageCode | None

Language for parsing the due date (e.g., 'en').

None
due_date date | None

The due date as a date object.

None
due_datetime datetime | None

The due date and time as a datetime object.

None
assignee_id str | None

User ID to whom the task is assigned.

None
day_order int | None

The order of the task inside Today or Next 7 days view.

None
collapsed bool | None

Whether the task's sub-tasks are collapsed.

None
duration int | None

The amount of time the task will take.

None
duration_unit Literal['minute', 'day'] | None

The unit of time for duration.

None
deadline_date date | None

The deadline date as a date object.

None
deadline_lang LanguageCode | None

Language for parsing the deadline date.

None

Returns:

Type Description
Task

the updated Task.

Raises:

Type Description
requests.exceptions.HTTPError

If the API request fails.

Source code in todoist_api_python/api.py
def update_task(  # noqa: PLR0912
    self,
    task_id: str,
    *,
    content: Annotated[str, MinLen(1), MaxLen(500)] | None = None,
    description: Annotated[str, MaxLen(16383)] | None = None,
    labels: list[Annotated[str, MaxLen(60)]] | None = None,
    priority: Annotated[int, Ge(1), Le(4)] | None = None,
    due_string: Annotated[str, MaxLen(150)] | None = None,
    due_lang: LanguageCode | None = None,
    due_date: date | None = None,
    due_datetime: datetime | None = None,
    assignee_id: str | None = None,
    day_order: int | None = None,
    collapsed: bool | None = None,
    duration: Annotated[int, Ge(1)] | None = None,
    duration_unit: Literal["minute", "day"] | None = None,
    deadline_date: date | None = None,
    deadline_lang: LanguageCode | None = None,
) -> Task:
    """
    Update an existing task.

    Only the fields to be updated need to be provided.

    :param task_id: The ID of the task to update.
    :param content: The text content of the task.
    :param description: Description for the task.
    :param labels: The task's labels (a list of names).
    :param priority: The priority of the task (4 for very urgent).
    :param due_string: The due date in natural language format.
    :param due_lang: Language for parsing the due date (e.g., 'en').
    :param due_date: The due date as a date object.
    :param due_datetime: The due date and time as a datetime object.
    :param assignee_id: User ID to whom the task is assigned.
    :param day_order: The order of the task inside Today or Next 7 days view.
    :param collapsed: Whether the task's sub-tasks are collapsed.
    :param duration: The amount of time the task will take.
    :param duration_unit: The unit of time for duration.
    :param deadline_date: The deadline date as a date object.
    :param deadline_lang: Language for parsing the deadline date.
    :return: the updated Task.
    :raises requests.exceptions.HTTPError: If the API request fails.
    """
    endpoint = get_api_url(f"{TASKS_PATH}/{task_id}")

    data: dict[str, Any] = {}
    if content is not None:
        data["content"] = content
    if description is not None:
        data["description"] = description
    if labels is not None:
        data["labels"] = labels
    if priority is not None:
        data["priority"] = priority
    if due_string is not None:
        data["due_string"] = due_string
    if due_lang is not None:
        data["due_lang"] = due_lang
    if due_date is not None:
        data["due_date"] = format_date(due_date)
    if due_datetime is not None:
        data["due_datetime"] = format_datetime(due_datetime)
    if assignee_id is not None:
        data["assignee_id"] = assignee_id
    if day_order is not None:
        data["day_order"] = day_order
    if collapsed is not None:
        data["collapsed"] = collapsed
    if duration is not None:
        data["duration"] = duration
    if duration_unit is not None:
        data["duration_unit"] = duration_unit
    if deadline_date is not None:
        data["deadline_date"] = format_date(deadline_date)
    if deadline_lang is not None:
        data["deadline_lang"] = deadline_lang

    task_data: dict[str, Any] = post(
        self._session, endpoint, self._token, data=data
    )
    return Task.from_dict(task_data)

Bases: Iterator[list[T]]

Iterator for paginated results from the Todoist API.

It encapsulates the logic for fetching and iterating through paginated results from Todoist API endpoints. It handles cursor-based pagination automatically, requesting new pages as needed when iterating.

Source code in todoist_api_python/api.py
class ResultsPaginator(Iterator[list[T]]):
    """
    Iterator for paginated results from the Todoist API.

    It encapsulates the logic for fetching and iterating through paginated results
    from Todoist API endpoints. It handles cursor-based pagination automatically,
    requesting new pages as needed when iterating.
    """

    _session: requests.Session
    _url: str
    _results_field: str
    _results_inst: Callable[[Any], T]
    _token: str
    _cursor: str | None

    def __init__(
        self,
        session: requests.Session,
        url: str,
        results_field: str,
        results_inst: Callable[[Any], T],
        token: str,
        params: dict[str, Any],
    ) -> None:
        """
        Initialize the ResultsPaginator.

        :param session: The requests Session to use for API calls.
        :param url: The API endpoint URL to fetch results from.
        :param results_field: The key in the API response that contains the results.
        :param results_inst: A callable that converts result items to objects of type T.
        :param token: The authentication token for the Todoist API.
        :param params: Query parameters to include in API requests.
        """
        self._session = session
        self._url = url
        self._results_field = results_field
        self._results_inst = results_inst
        self._token = token
        self._params = params
        self._cursor = ""  # empty string for first page

    def __next__(self) -> list[T]:
        """
        Fetch and return the next page of results from the Todoist API.

        :return: A list of results.
        :raises requests.exceptions.HTTPError: If the API request fails.
        :raises TypeError: If the API response structure is unexpected.
        """
        if self._cursor is None:
            raise StopIteration

        params = self._params.copy()
        if self._cursor != "":
            params["cursor"] = self._cursor

        data: dict[str, Any] = get(self._session, self._url, self._token, params)
        self._cursor = data.get("next_cursor")

        results: list[Any] = data.get(self._results_field, [])
        return [self._results_inst(result) for result in results]