Records and results¶
These are what reads and writes give you back, and what you pass to write a file.
The examples below use tasks, made as shown here, and app, which is qb.app("bq1234567"). Job Tasks has Title, Status, Hours, Due Date, Code, which is unique, and Attachment. It starts with three records, and each example starts from those again. A live test runs every example with both clients and checks its output.
from pyquickbase import Quickbase
with Quickbase(
realm="acme", token="your_user_token"
) as qb:
tasks = qb.app("bq1234567").table("Job Tasks")
from pyquickbase import AsyncQuickbase
async with AsyncQuickbase(
realm="acme", token="your_user_token"
) as qb:
tasks = qb.app("bq1234567").table("Job Tasks")
Read results¶
For the full picture, see Reading records.
Record¶
One record. Read values by field label or ID: record["Status"] or record[7].
Iterating yields labels, so dict(record) gives {label: value}. Date fields are date objects and timestamp fields are timezone-aware datetime objects, or None when empty. A user field holds a User, a multiuser field a list of them, and a file field a FileAttachment, or None when empty.
Attributes
idint
The Record ID#. Queries always select it, but a report may not include it.
Example
task = tasks.get(
1, select=["Title", "Status", "Due Date"]
)
print(task.id, task["Title"], task[7])
print(task["Due Date"])
print(sorted(label for label in task))
task = await tasks.get(
1, select=["Title", "Status", "Due Date"]
)
print(task.id, task["Title"], task[7])
print(task["Due Date"])
print(sorted(label for label in task))
Output
1 Pour slab Open
2027-01-15
['Due Date', 'Record ID#', 'Status', 'Title']
RecordIterator¶
Yields records from a query, fetching further pages as needed.
A class rather than an async generator, so stopping early with break needs no cleanup.
Example
records = tasks.query(select=["Title"], max_total=2)
for task in records:
print(task.id, task["Title"])
records = tasks.query(select=["Title"], max_total=2)
async for task in records:
print(task.id, task["Title"])
Output
1 Pour slab
2 Frame walls
Write records to a CSV file¶
Write the remaining records to a CSV file as they're fetched, with a header row.
Dates and timestamps are ISO 8601, with a timestamp's offset. A user is their email, or their ID if the client's policy hides emails, a file is its latest version's name, and a list's values are separated by ;. rows_from_file() reads the file back.
Parameters
destinationFileLike, required
A path, or a file open for writing text.spreadsheet_safebool, defaultTrue
Write text that a spreadsheet would run as a formula, which starts with=,+,-, or@, with a leading', so opening the file can't run it.rows_from_file()removes it again.encodingstr, default'utf-8-sig'
The text encoding for a path. The default writes a byte order mark, which Excel needs to read UTF-8.
Returns int
How many records were written.
to_csv(
destination: FileLike,
*,
spreadsheet_safe: bool = True,
encoding: str = "utf-8-sig",
) -> int
Write records to an Excel file¶
Write the remaining records to an Excel file, with a header row. Needs pyquickbase[excel].
Dates and numbers are Excel's own, and a timestamp is written in UTC, as Excel has no time zones. Text is always text, so a value starting with = is never a formula. Values are written as for to_csv().
Parameters
destinationFileLike, required
A path, or a file open for writing bytes.sheetstr, default'Records'
The worksheet's name.
Returns int
How many records were written.
to_excel(
destination: FileLike, *, sheet: str = "Records"
) -> int
Fetch records into pandas¶
Fetch the remaining records into a pandas DataFrame, with a column for each field.
A user field holds the user's email, or their ID if the client's policy hides emails, and a file field holds the latest version's file name. Needs pyquickbase[pandas].
Returns pandas.DataFrame
A DataFrame, which has the columns even when nothing matched.
to_pandas() -> pandas.DataFrame
Fetch records into polars¶
Fetch the remaining records into a polars DataFrame. See to_pandas(). Needs pyquickbase[polars].
Returns polars.DataFrame
A DataFrame, which has the columns even when nothing matched.
to_polars() -> polars.DataFrame
Page¶
One page of query results.
Attributes
recordstuple[Record, ...]
TheRecordobjects on this page.totalint
How many records match, not just the ones on this page.skipint
How many matching records came before this page.columnstuple[str, ...]
The labels of the fields it returned, in order, even when it has no records.has_morebool
Whether more matching records come after this page.next_skipint
Theskipthat fetches the following page.
Example
page = tasks.query_page(limit=2)
print(len(page.records), page.total, page.skip)
print(page.has_more, page.next_skip)
page = await tasks.query_page(limit=2)
print(len(page.records), page.total, page.skip)
print(page.has_more, page.next_skip)
Output
2 3 0
True 2
Write a page to a CSV file¶
Write this page's records to a CSV file, with a header row. See RecordIterator.to_csv().
Parameters
destinationFileLike, required
A path, or a file open for writing text.spreadsheet_safebool, defaultTrue
Write text that a spreadsheet would run as a formula with a leading'.encodingstr, default'utf-8-sig'
The text encoding for a path.
Returns int
How many records were written.
to_csv(
destination: FileLike,
*,
spreadsheet_safe: bool = True,
encoding: str = "utf-8-sig",
) -> int
Write a page to an Excel file¶
Write this page's records to an Excel file, with a header row. See RecordIterator.to_excel(). Needs pyquickbase[excel].
Parameters
destinationFileLike, required
A path, or a file open for writing bytes.sheetstr, default'Records'
The worksheet's name.
Returns int
How many records were written.
to_excel(
destination: FileLike, *, sheet: str = "Records"
) -> int
Turn a page into pandas¶
This page's records as a pandas DataFrame, with a column for each field returned.
A user field holds the user's email, or their ID if the client's policy hides emails, and a file field holds the latest version's file name. Needs pyquickbase[pandas].
Returns pandas.DataFrame
A DataFrame, which has the columns even when the page has no records.
to_pandas() -> pandas.DataFrame
Turn a page into polars¶
This page's records as a polars DataFrame. See to_pandas(). Needs pyquickbase[polars].
Returns polars.DataFrame
A DataFrame, which has the columns even when the page has no records.
to_polars() -> polars.DataFrame
User¶
A Quickbase user, as a user or multiuser field holds one.
repr leaves out the email address, because reprs end up in logs. Pass a User as a value to write it to a user field, or to compare with one in F.
Attributes
idstr
The user's ID.namestr
The user's name, as Quickbase shows it.emailstr | None
The user's email address, or None if the client's policy leaves it out.user_namestr | None
The user's Quickbase user name, if Quickbase sent one.
Example
from pyquickbase import User
task = tasks.get(1, select=["Record Owner"])
owner = task["Record Owner"]
print(isinstance(owner, User), bool(owner.email))
from pyquickbase import User
task = await tasks.get(1, select=["Record Owner"])
owner = task["Record Owner"]
print(isinstance(owner, User), bool(owner.email))
Output
True True
Write results¶
For the full picture, see Writing records.
WriteResult¶
What a write did. If rows fail, WriteError.result holds this, including what succeeded.
Attributes
created_idstuple[int, ...]
The Record ID# of each new record, in the order of the rows that succeeded. Userow_idsto match them to your rows.updated_idstuple[int, ...]
The records whose values changed.unchanged_idstuple[int, ...]
The records that already held these values.errorstuple[RowError, ...]
ARowErrorfor each row that failed.row_idstuple[int | None, ...]
Forinsert(), the new Record ID# of each row, in the order of your rows, or None for a row that failed. It stops short at a batch that wasn't sent, or whose response can't be matched to its rows, so compare its length with your rows. Empty forupdate()andupsert().
Example
result = tasks.upsert(
[
{"Code": "T-1", "Status": "Open"},
{"Code": "T-2", "Status": "Done"},
{"Code": "T-9", "Title": "Inspect site"},
],
merge_on="Code",
)
print(result.created_ids)
print(result.updated_ids)
print(result.unchanged_ids)
print(result.errors)
result = await tasks.upsert(
[
{"Code": "T-1", "Status": "Open"},
{"Code": "T-2", "Status": "Done"},
{"Code": "T-9", "Title": "Inspect site"},
],
merge_on="Code",
)
print(result.created_ids)
print(result.updated_ids)
print(result.unchanged_ids)
print(result.errors)
Output
(4,)
(2,)
(1,)
()
RowError¶
Why one row failed.
Attributes
indexint
The row's position in the rows you passed, starting from 0.messagestuple[str, ...]
Quickbase's wording, which can change, so usereasonto decide what to do. For arulefailure, this holds the rule's message.reasonLiteral['duplicate', 'permission', 'invalid_choice', 'rule', 'other']
duplicatefor a value already in a unique field,permissionfor a refusal by the token's role,invalid_choicefor a value outside a field's choices,rulefor a refusal by one of the table's custom data rules, andotherfor anything else, including wording this version doesn't recognize.field_idint | None
The field Quickbase named, forduplicateandinvalid_choice.
Example
from pyquickbase import WriteError
try:
tasks.insert(
[
{"Title": "New", "Code": "T-8"},
{"Title": "Repeat", "Code": "T-1"},
]
)
except WriteError as error:
print(error.result.created_ids)
for failure in error.result.errors:
print(failure.index, failure.reason, failure.field_id)
from pyquickbase import WriteError
try:
await tasks.insert(
[
{"Title": "New", "Code": "T-8"},
{"Title": "Repeat", "Code": "T-1"},
]
)
except WriteError as error:
print(error.result.created_ids)
for failure in error.result.errors:
print(failure.index, failure.reason, failure.field_id)
Output
(4,)
1 duplicate 10
Files¶
For the full picture, see File attachments.
FileUpload¶
A file to write to a file attachment field, as a value in insert(), update(), or upsert().
repr leaves out the contents, because they may be sensitive and reprs end up in logs.
Parameters
file_namestr, required
The file's name. It can't contain a path or control characters, because Quickbase stores them as sent and could pass them to whoever downloads the file later.databytes, required
The file's contents.
Raises
InvalidValueError:file_nameis empty, or contains a path or control characters.
FileUpload(file_name: str, data: bytes)
Example
from pyquickbase import FileUpload
upload = FileUpload("spec.txt", b"Pour on Monday")
result = tasks.update(
[{"Record ID#": 1, "Attachment": upload}]
)
print(result.updated_ids)
from pyquickbase import FileUpload
upload = FileUpload("spec.txt", b"Pour on Monday")
result = await tasks.update(
[{"Record ID#": 1, "Attachment": upload}]
)
print(result.updated_ids)
Output
(1,)
FileDownload¶
A file downloaded from a file attachment field.
repr leaves out the contents, because they may be sensitive and reprs end up in logs.
Attributes
file_namestr
The file's name. Quickbase stores names as sent, such as../../x, so don't use this as a path without cleaning it.databytes
The file's contents.content_typestr | None
The media type, if reported. It's based on the name rather than the contents, so it doesn't tell you what's in the file.
Example
from pyquickbase import FileUpload
upload = FileUpload("spec.txt", b"Pour on Monday")
tasks.update([{"Record ID#": 1, "Attachment": upload}])
download = tasks.download_file(1, "Attachment")
print(download.file_name, download.data)
from pyquickbase import FileUpload
upload = FileUpload("spec.txt", b"Pour on Monday")
await tasks.update([{"Record ID#": 1, "Attachment": upload}])
download = await tasks.download_file(1, "Attachment")
print(download.file_name, download.data)
Output
spec.txt b'Pour on Monday'
FileAttachment¶
A file attachment field's value, which holds each version Quickbase keeps.
An empty field reads as None, so a FileAttachment always has at least one version. Download the contents with table.download_file().
Attributes
urlstr
The file's path in Quickbase, relative to the realm.versionstuple[FileVersion, ...]
Each version Quickbase keeps, oldest first.latestFileVersion
The newest version.
Example
from pyquickbase import FileUpload
upload = FileUpload("plan.txt", b"v1")
tasks.update([{"Record ID#": 2, "Attachment": upload}])
task = tasks.get(2, select=["Attachment"])
attachment = task["Attachment"]
print(attachment.latest.file_name, len(attachment.versions))
from pyquickbase import FileUpload
upload = FileUpload("plan.txt", b"v1")
await tasks.update([{"Record ID#": 2, "Attachment": upload}])
task = await tasks.get(2, select=["Attachment"])
attachment = task["Attachment"]
print(attachment.latest.file_name, len(attachment.versions))
Output
plan.txt 1
FileVersion¶
One version of a file in a file attachment field.
Attributes
numberint
The version's number, starting at 1. Numbers aren't reused after a delete.file_namestr
The file's name. Quickbase stores names as sent, such as../../x, so don't use this as a path without cleaning it.uploadeddatetime.datetime | None
When it was uploaded, or None if Quickbase didn't say.creatorUser | None
Who uploaded it, or None if the client's policy leaves it out.
Example
from pyquickbase import FileUpload
upload = FileUpload("notes.txt", b"v1")
tasks.update([{"Record ID#": 3, "Attachment": upload}])
task = tasks.get(3, select=["Attachment"])
latest = task["Attachment"].latest
print(latest.number, latest.file_name)
from pyquickbase import FileUpload
upload = FileUpload("notes.txt", b"v1")
await tasks.update([{"Record ID#": 3, "Attachment": upload}])
task = await tasks.get(3, select=["Attachment"])
latest = task["Attachment"].latest
print(latest.number, latest.file_name)
Output
1 notes.txt
CSV and Excel files¶
For the full picture, see CSV and Excel files.
FileRows¶
A file's rows, converted for a table's fields, from Table.rows_from_file().
Pass it to insert(), update(), upsert(), or preview_update(), which refuse it, writing nothing, while it has errors. Iterating it gives its rows.
repr shows only its size, because values may be sensitive and reprs end up in logs.
Attributes
rowstuple[Mapping[str, Any], ...]
Each row that has a value, as{field label: value}. A blank cell is None, which leaves a field empty ininsert(), and clears it inupdate()andupsert().row_numberstuple[int, ...]
Each row's number in the file, as forFileRowError.row, sorows[0]came from rowrow_numbers[0]. A row whose cells are all blank is left out.columnsMapping[str, str]
Which field each column was read into, as{column: field label}.skippedtuple[str, ...]
The columns that weren't read.errorstuple[FileRowError, ...]
Every value that can't be written. Fix these in the file, or map or skip their columns.okbool
Whether every value can be written.
Example
import io
text = io.StringIO('Title,Hours\nA,"1,234.5"\nB,lots\n')
rows = tasks.rows_from_file(text)
print(rows.rows[0]["Hours"], rows.ok)
print(rows.columns)
import io
text = io.StringIO('Title,Hours\nA,"1,234.5"\nB,lots\n')
rows = await tasks.rows_from_file(text)
print(rows.rows[0]["Hours"], rows.ok)
print(rows.columns)
Output
1234.5 False
{'Title': 'Title', 'Hours': 'Hours'}
Refuse a file with errors¶
Raise InvalidValueError listing the first errors, if there are any.
Raises
InvalidValueError: The file has values that can't be written.
raise_for_errors() -> None
FileRowError¶
A value in a file that can't be written to its field.
Attributes
rowint
The row's number as a spreadsheet shows it, counting the header as row 1.columnstr
The file's column.messagestr
What's wrong, which never repeats the value.
Example
import io
text = io.StringIO("Title,Due Date\nA,next week\n")
rows = tasks.rows_from_file(text)
for error in rows.errors:
print(error.row, error.column)
print(error.message)
import io
text = io.StringIO("Title,Due Date\nA,next week\n")
rows = await tasks.rows_from_file(text)
for error in rows.errors:
print(error.row, error.column)
print(error.message)
Output
2 Due Date
isn't a date in the form YYYY-MM-DD. Set date_format for another.
FileFormat¶
A file's format: csv, including tab-separated files, or xlsx.
FileFormat = Literal["csv", "xlsx"]
FileLike¶
A path, or an open file: text for CSV, and binary for Excel.
FileLike = str | os.PathLike[str] | IO[str] | IO[bytes]
ColumnMap¶
Which field each file column goes in, by label or ID, or None to skip it.
ColumnMap = Mapping[str, str | int | None]
Formula results¶
For the full picture, see Formulas.
FormulaResult¶
The result of table.run_formula().
Attributes
textstr
Exactly what Quickbase sent, which is always a string.typestr
Quickbase's name for its type, such asnumber,bool, ordate.valueAny
The result as a Python value, converted only where that's safe. AboolbecomesTrueorFalse, and anumberbecomes aDecimal, or None if either is empty. Anullbecomes None. Anything else stays as text, including dates and timestamps, because they use the app's display format and have no time zone.
Example
result = tasks.run_formula("1 + 1")
print(result.type, result.text, result.value)
result = await tasks.run_formula("1 + 1")
print(result.type, result.text, result.value)
Output
number 2 2
Previews of writes¶
For the full picture, see Writing records.
UpdatePreview¶
What update() would do if the records stayed as they are now.
Attributes
changestuple[RecordChange, ...]
The records that would change, in the order of your rows.unchanged_idstuple[int, ...]
The records that already hold these values.missing_idstuple[int, ...]
The records that don't exist, or that the token can't see.update()would report these as failed rows.eventstuple[AppEvent, ...] | None
The active webhooks, Quickbase actions, email notifications, and automations on the table, which the update may set off. Quickbase doesn't say which changes set each one off, so some may not run. None if they couldn't be read.
Example
preview = tasks.preview_update(
[
{"Record ID#": 1, "Status": "Done"},
{"Record ID#": 2, "Status": "Open"},
{"Record ID#": 999, "Status": "Done"},
]
)
print(len(preview.changes))
print(preview.unchanged_ids, preview.missing_ids)
preview = await tasks.preview_update(
[
{"Record ID#": 1, "Status": "Done"},
{"Record ID#": 2, "Status": "Open"},
{"Record ID#": 999, "Status": "Done"},
]
)
print(len(preview.changes))
print(preview.unchanged_ids, preview.missing_ids)
Output
1
(2,) (999,)
DeletePreview¶
What delete() would delete: a page of the matching records, and what may run.
Attributes
recordstuple[Record, ...]
The first records that would be deleted.totalint
How many records would be deleted.skipint
How many matching records came before this page.columnstuple[str, ...]
The labels of the fields it returned, in order.eventstuple[AppEvent, ...] | None
The active webhooks, Quickbase actions, email notifications, and automations on the table, which deleting records may set off. Quickbase doesn't say which changes set each one off, so some may not run. None if they couldn't be read.
Example
from pyquickbase import F
preview = tasks.preview_delete(F("Status") == "Done")
print(preview.total, preview.events)
from pyquickbase import F
preview = await tasks.preview_delete(F("Status") == "Done")
print(preview.total, preview.events)
Output
1 ()
RecordChange¶
What a write would change on one record.
Attributes
record_idint
The record's Record ID#.fieldsMapping[str, FieldChange]
What would change, as{field label: FieldChange}.
Example
preview = tasks.preview_update(
[{"Record ID#": 1, "Status": "Done"}]
)
change = preview.changes[0]
print(change.record_id, sorted(change.fields))
preview = await tasks.preview_update(
[{"Record ID#": 1, "Status": "Done"}]
)
change = preview.changes[0]
print(change.record_id, sorted(change.fields))
Output
1 ['Status']
FieldChange¶
One field's value before and after a write, with an empty field as None.
repr hides both values, because they may be sensitive and reprs end up in logs.
Attributes
beforeAny
The value now. A file field shows the file's name, never its contents.afterAny
The value the write would set. A file field shows the file's name here too.
Example
preview = tasks.preview_update(
[{"Record ID#": 1, "Status": "Done"}]
)
status = preview.changes[0].fields["Status"]
print(status.before, status.after)
preview = await tasks.preview_update(
[{"Record ID#": 1, "Status": "Done"}]
)
status = preview.changes[0].fields["Status"]
print(status.before, status.after)
Output
Open Done
Summaries¶
For the full picture, see Reading records.
SummaryRow¶
One group of records, from table.summarize(). Values are keyed by field label.
repr shows only the row's shape, because values may be sensitive and reprs end up in logs.
Attributes
groupMapping[str, Any]
The values that define this group, or empty withoutgroup_by. An empty value is None, and a user is their email.countint
How many records are in the group, including those with empty values.sumMapping[str, float]
The sum of eachsumfield, which is 0 if every value is empty.avgMapping[str, float | None]
The average of eachavgfield, or None if every value is empty.medianMapping[str, float | None]
The middle value of eachmedianfield, or the average of the two middle ones, or None if every value is empty.std_devMapping[str, float | None]
The sample standard deviation of eachstd_devfield, or None with fewer than two values.minMapping[str, Any]
The lowest value of eachminfield, or None if every value is empty.maxMapping[str, Any]
The highest value of eachmaxfield, or None if every value is empty.distinct_countMapping[str, int]
How many different values eachdistinct_countfield has, leaving out empty ones.changePeriodChange | None
Withcompare_periods, how the group changed since the period before. None for the first period, and withoutcompare_periods.
Example
rows = tasks.summarize(
"Status", sum="Hours", median="Hours"
)
for row in rows:
print(row.group["Status"], row.count)
print(row.sum["Hours"], row.median["Hours"])
rows = await tasks.summarize(
"Status", sum="Hours", median="Hours"
)
for row in rows:
print(row.group["Status"], row.count)
print(row.sum["Hours"], row.median["Hours"])
Output
Done 1
6.0 6.0
Open 2
20.0 10.0
Get one statistic's values¶
One statistic's values, by field label, such as row.statistic("sum").
Parameters
nameStatisticName, required
The statistic, such assumormedian.
Returns Mapping[str, Any]
Its values, by field label, which is empty if the summary didn't ask for it.
statistic(name: StatisticName) -> Mapping[str, Any]
PeriodChange¶
How a group changed since the period before it, from summarize(compare_periods=True).
Attributes
previousstr
The period it's compared with, such as2026-02for2026-03.countint
How many more records there are, which is negative for fewer.valuesMapping[str, Mapping[str, float | None]]
How much each numeric statistic rose, as{"sum": {"Hours": 4.0}}, or None where either period has no value. A period without records counts as 0 forsumanddistinct_count, and has no value for the others.
Example
from pyquickbase import Period
rows = tasks.summarize(
Period("Due Date", "month"),
sum="Hours",
compare_periods=True,
)
for row in rows:
if row.change:
print(row.group["Due Date"], row.change.previous)
print(row.change.count, row.change.values["sum"])
from pyquickbase import Period
rows = await tasks.summarize(
Period("Due Date", "month"),
sum="Hours",
compare_periods=True,
)
for row in rows:
if row.change:
print(row.group["Due Date"], row.change.previous)
print(row.change.count, row.change.values["sum"])
Output
2027-02 2027-01
0 {'Hours': 4.0}
Period¶
Group by a date or timestamp field's day, week, month, quarter, or year.
Groups are labeled 2026-03-14, 2026-W11 (an ISO week), 2026-03, 2026-Q1, and 2026, so they sort in order.
Parameters
fieldstr | int | F, required
The date or timestamp field, by label, ID, or field object.unitPeriodUnit, required
day,week,month,quarter, oryear.tzdatetime.tzinfo, defaultdatetime.timezone.utc
The time zone that decides a timestamp's date. It has no effect on a date field.
Raises
QueryError:unitisn't one of these, orfieldisn't a field.
Period(
field: str | int | F,
unit: PeriodUnit,
tz: datetime.tzinfo = datetime.timezone.utc,
)
Example
from pyquickbase import Period
rows = tasks.summarize(Period("Due Date", "month"))
for row in rows:
print(row.group["Due Date"], row.count)
from pyquickbase import Period
rows = await tasks.summarize(Period("Due Date", "month"))
for row in rows:
print(row.group["Due Date"], row.count)
Output
2027-01 1
2027-02 1
None 1
PeriodUnit = Literal[
"day", "week", "month", "quarter", "year"
]
StatisticName¶
A value summarize() works out from one field's values in each group.
StatisticName = Literal[
"sum",
"avg",
"median",
"std_dev",
"min",
"max",
"distinct_count",
]
SummaryGroups¶
What Table.summarize() can group by.
SummaryGroups = (
str | int | F | Period | Sequence[str | int | F | Period]
)