[{"data":1,"prerenderedAt":1586},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fgoogle-sheets-vs-excel-files-for-python-automation":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fgoogle-sheets-vs-excel-files-for-python-automation":1553,"ancestors:\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fgoogle-sheets-vs-excel-files-for-python-automation":1561,"children:\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fgoogle-sheets-vs-excel-files-for-python-automation":1566,"related:\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fgoogle-sheets-vs-excel-files-for-python-automation":1567},{"id":4,"title":5,"body":6,"dateModified":1519,"datePublished":1519,"description":1520,"extension":1521,"faq":1522,"keyword":1531,"kind":1532,"meta":1533,"navigation":88,"path":1542,"seo":1543,"slug":1546,"stem":1547,"topics":1548,"type":1551,"__hash__":1552},"docs\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fgoogle-sheets-vs-excel-files-for-python-automation\u002Findex.md","Google Sheets vs Excel Files for Python Automation",{"type":7,"value":8,"toc":1506},"minimark",[9,24,29,63,66,138,141,145,256,298,302,305,550,569,712,727,731,744,837,852,952,960,964,971,1004,1036,1040,1043,1189,1192,1198,1202,1229,1363,1367,1426,1430,1438,1442,1448,1454,1460,1466,1470,1502],[10,11,12,13,17,18,23],"p",{},"Many teams now live in Google Sheets as much as in Excel, and a Python report can target either: write an ",[14,15,16],"code",{},".xlsx"," file and send or upload it, or update a shared Google Sheet in place through the Sheets API. The choice changes more than the library. It decides how people get the report, what can go wrong at 3 a.m., and how numbers survive the trip. This page compares the two for automation, with verified examples of the mistakes the Sheets route invites. It is part of ",[19,20,22],"a",{"href":21},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002F","Choosing a Python Excel Library",".",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install gspread pandas openpyxl xlsxwriter\n","bash","",[14,37,38],{"__ignoreMap":35},[39,40,43,47,51,54,57,60],"span",{"class":41,"line":42},"line",1,[39,44,46],{"class":45},"sMTad","pip",[39,48,50],{"class":49},"srMev"," install",[39,52,53],{"class":49}," gspread",[39,55,56],{"class":49}," pandas",[39,58,59],{"class":49}," openpyxl",[39,61,62],{"class":49}," xlsxwriter\n",[10,64,65],{},"The Google Sheets examples use gspread 6. They run here against a local stand-in for the Sheets API, which records every request, so the exact payloads can be shown without a Google account. Against the real service you would authenticate with a service account first:",[30,67,71],{"className":68,"code":69,"language":70,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","import gspread\n\ngc = gspread.service_account(filename=\"service_account.json\")   # a Google Cloud service account key\nsh = gc.open_by_key(\"1AbCdEf...\")                                # the ID from the sheet's URL\n","python",[14,72,73,83,90,118],{"__ignoreMap":35},[39,74,75,79],{"class":41,"line":42},[39,76,78],{"class":77},"s-kum","import",[39,80,82],{"class":81},"skGVy"," gspread\n",[39,84,86],{"class":41,"line":85},2,[39,87,89],{"emptyLinePlaceholder":88},true,"\n",[39,91,93,96,99,102,106,108,111,114],{"class":41,"line":92},3,[39,94,95],{"class":81},"gc ",[39,97,98],{"class":77},"=",[39,100,101],{"class":81}," gspread.service_account(",[39,103,105],{"class":104},"sa561","filename",[39,107,98],{"class":77},[39,109,110],{"class":49},"\"service_account.json\"",[39,112,113],{"class":81},")   ",[39,115,117],{"class":116},"s-wDw","# a Google Cloud service account key\n",[39,119,121,124,126,129,132,135],{"class":41,"line":120},4,[39,122,123],{"class":81},"sh ",[39,125,98],{"class":77},[39,127,128],{"class":81}," gc.open_by_key(",[39,130,131],{"class":49},"\"1AbCdEf...\"",[39,133,134],{"class":81},")                                ",[39,136,137],{"class":116},"# the ID from the sheet's URL\n",[10,139,140],{},"The service account has its own email address; share the spreadsheet with it as an editor, or it gets a permission error.",[25,142,144],{"id":143},"the-main-differences","The main differences",[146,147,155,156,155,160,155,164,155,172,155,182,155,193,155,200,155,206,155,211,155,215,155,219,155,223,155,228,155,235,155,240,155,244,155,247,155,250,155,253],"svg",{"viewBox":148,"role":149,"ariaLabelledBy":150,"xmlns":153,"style":154},"4 0 752 233","img",[151,152],"gs-overview-t","gs-overview-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[157,158,159],"title",{"id":151},"A file you send, or a sheet people open",[161,162,163],"desc",{"id":152},"Two panels. An Excel file is written locally by openpyxl or xlsxwriter, needs no credentials, supports the full formatting and chart model, and is delivered by email or upload. A Google Sheet is updated in place through an API, needs a service account, has quotas and a 10 million cell limit, and everyone sees the latest version at the same link.",[165,166],"rect",{"x":167,"y":168,"width":169,"height":170,"fill":171},"4","0","752","233","#ffffff",[165,173],{"x":174,"y":175,"width":176,"height":177,"rx":178,"fill":179,"stroke":180,"style":181},"20.0","16.0","351.0","201.0","14","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[183,184,186],"g",{"transform":185},"translate(182.5,29.0) scale(1.083)",[187,188],"path",{"d":189,"fill":190,"stroke":191,"style":192},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6","none","var(--brand-strong,#4338ca)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[194,195,199],"text",{"x":196,"y":197,"style":198},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Excel file",[41,201],{"x1":202,"y1":203,"x2":204,"y2":203,"stroke":180,"style":205},"36.0","91.0","355.0","stroke-width:1px",[194,207,210],{"x":196,"y":208,"style":209},"111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","written locally, no network",[194,212,214],{"x":196,"y":213,"style":209},"137.0","full formats, charts, tables",[194,216,218],{"x":196,"y":217,"style":209},"163.0","delivered as a copy",[194,220,222],{"x":196,"y":221,"style":209},"189.0","1,048,576 rows per sheet",[165,224],{"x":225,"y":175,"width":176,"height":177,"rx":178,"fill":226,"stroke":227,"style":181},"389.0","#d9f4f1","var(--teal,#0f9488)",[183,229,231],{"transform":230},"translate(551.5,29.0) scale(1.083)",[187,232],{"d":233,"fill":190,"stroke":234,"style":192},"M7 18h10a4 4 0 0 0 .6-8A6 6 0 0 0 6.1 11.4 3.4 3.4 0 0 0 7 18z","var(--teal-ink,#0b6157)",[194,236,239],{"x":237,"y":197,"style":238},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Google Sheet",[41,241],{"x1":242,"y1":203,"x2":243,"y2":203,"stroke":227,"style":205},"405.0","724.0",[194,245,246],{"x":237,"y":208,"style":209},"updated in place via API",[194,248,249],{"x":237,"y":213,"style":209},"service account + quotas",[194,251,252],{"x":237,"y":217,"style":209},"one live link, history",[194,254,255],{"x":237,"y":221,"style":209},"10 million cells per file",[257,258,259,270,276,286,292],"ul",{},[260,261,262,266,267,269],"li",{},[263,264,265],"strong",{},"Access."," An ",[14,268,16],{}," needs nothing but a library; the job can run offline. A Google Sheet needs a Google Cloud project, the Sheets API enabled, a service account key stored safely, and network access at run time.",[260,271,272,275],{},[263,273,274],{},"Limits."," Excel sheets hold 1,048,576 rows by 16,384 columns. A Google spreadsheet holds up to 10 million cells across all its tabs, which a 26-column export reaches at about 380,000 rows.",[260,277,278,281,282,285],{},[263,279,280],{},"Rate limits."," The Sheets API has per-minute quotas on read and write requests per project and per user, documented in Google's usage limits. A script that updates cells one at a time hits them quickly; one ",[14,283,284],{},"update"," call per block of cells does not.",[260,287,288,291],{},[263,289,290],{},"Delivery and versions."," A file is a copy, so recipients may keep working on last week's. A shared sheet is always current, with version history built in, but anyone with edit access can change it between runs.",[260,293,294,297],{},[263,295,296],{},"Formatting."," openpyxl and xlsxwriter cover Excel's full model: number formats, conditional formats, charts, tables, data validation. gspread can format ranges and the API can do much of the rest, but each is a separate request, and charts via the API are verbose.",[25,299,301],{"id":300},"writing-a-dataframe-what-breaks","Writing a DataFrame: what breaks",[10,303,304],{},"Writing to Excel from pandas is one line, and types come through: dates stay dates, missing values become empty cells. The Sheets API takes JSON, and JSON has no dates and no NaN:",[30,306,308],{"className":68,"code":307,"language":70,"meta":35,"style":35},"import numpy as np\nimport pandas as pd\n\nws = gc.open_by_key(\"1AbCdEf\").worksheet(\"Report\")\ndf = pd.DataFrame({\"Week\": pd.to_datetime([\"2026-09-21\", \"2026-09-28\"]),\n                   \"Region\": [\"North\", \"South\"], \"Sales\": [48250.5, np.nan]})\n\nfor attempt in ([df.columns.tolist()] + df.values.tolist(),\n                [[\"Region\", \"Sales\"], [\"South\", float(\"nan\")]]):\n    try:\n        ws.update(attempt, \"A1\")\n    except Exception as e:\n        print(type(e).__name__, \"-\", str(e)[:70])\n",[14,309,310,323,335,339,360,389,421,426,447,479,488,499,514],{"__ignoreMap":35},[39,311,312,314,317,320],{"class":41,"line":42},[39,313,78],{"class":77},[39,315,316],{"class":81}," numpy ",[39,318,319],{"class":77},"as",[39,321,322],{"class":81}," np\n",[39,324,325,327,330,332],{"class":41,"line":85},[39,326,78],{"class":77},[39,328,329],{"class":81}," pandas ",[39,331,319],{"class":77},[39,333,334],{"class":81}," pd\n",[39,336,337],{"class":41,"line":92},[39,338,89],{"emptyLinePlaceholder":88},[39,340,341,344,346,348,351,354,357],{"class":41,"line":120},[39,342,343],{"class":81},"ws ",[39,345,98],{"class":77},[39,347,128],{"class":81},[39,349,350],{"class":49},"\"1AbCdEf\"",[39,352,353],{"class":81},").worksheet(",[39,355,356],{"class":49},"\"Report\"",[39,358,359],{"class":81},")\n",[39,361,363,366,368,371,374,377,380,383,386],{"class":41,"line":362},5,[39,364,365],{"class":81},"df ",[39,367,98],{"class":77},[39,369,370],{"class":81}," pd.DataFrame({",[39,372,373],{"class":49},"\"Week\"",[39,375,376],{"class":81},": pd.to_datetime([",[39,378,379],{"class":49},"\"2026-09-21\"",[39,381,382],{"class":81},", ",[39,384,385],{"class":49},"\"2026-09-28\"",[39,387,388],{"class":81},"]),\n",[39,390,392,395,398,401,403,406,409,412,414,418],{"class":41,"line":391},6,[39,393,394],{"class":49},"                   \"Region\"",[39,396,397],{"class":81},": [",[39,399,400],{"class":49},"\"North\"",[39,402,382],{"class":81},[39,404,405],{"class":49},"\"South\"",[39,407,408],{"class":81},"], ",[39,410,411],{"class":49},"\"Sales\"",[39,413,397],{"class":81},[39,415,417],{"class":416},"sP0c6","48250.5",[39,419,420],{"class":81},", np.nan]})\n",[39,422,424],{"class":41,"line":423},7,[39,425,89],{"emptyLinePlaceholder":88},[39,427,429,432,435,438,441,444],{"class":41,"line":428},8,[39,430,431],{"class":77},"for",[39,433,434],{"class":81}," attempt ",[39,436,437],{"class":77},"in",[39,439,440],{"class":81}," ([df.columns.tolist()] ",[39,442,443],{"class":77},"+",[39,445,446],{"class":81}," df.values.tolist(),\n",[39,448,450,453,456,458,460,463,465,467,470,473,476],{"class":41,"line":449},9,[39,451,452],{"class":81},"                [[",[39,454,455],{"class":49},"\"Region\"",[39,457,382],{"class":81},[39,459,411],{"class":49},[39,461,462],{"class":81},"], [",[39,464,405],{"class":49},[39,466,382],{"class":81},[39,468,469],{"class":416},"float",[39,471,472],{"class":81},"(",[39,474,475],{"class":49},"\"nan\"",[39,477,478],{"class":81},")]]):\n",[39,480,482,485],{"class":41,"line":481},10,[39,483,484],{"class":77},"    try",[39,486,487],{"class":81},":\n",[39,489,491,494,497],{"class":41,"line":490},11,[39,492,493],{"class":81},"        ws.update(attempt, ",[39,495,496],{"class":49},"\"A1\"",[39,498,359],{"class":81},[39,500,502,505,508,511],{"class":41,"line":501},12,[39,503,504],{"class":77},"    except",[39,506,507],{"class":416}," Exception",[39,509,510],{"class":77}," as",[39,512,513],{"class":81}," e:\n",[39,515,517,520,522,525,528,531,533,536,538,541,544,547],{"class":41,"line":516},13,[39,518,519],{"class":416},"        print",[39,521,472],{"class":81},[39,523,524],{"class":416},"type",[39,526,527],{"class":81},"(e).",[39,529,530],{"class":416},"__name__",[39,532,382],{"class":81},[39,534,535],{"class":49},"\"-\"",[39,537,382],{"class":81},[39,539,540],{"class":416},"str",[39,542,543],{"class":81},"(e)[:",[39,545,546],{"class":416},"70",[39,548,549],{"class":81},"])\n",[10,551,552,553,556,557,560,561,564,565,568],{},"Both fail before anything is sent, which is the good case: a job that crashes gets noticed. The same error appears for a single value taken from a DataFrame with ",[14,554,555],{},".iloc"," or ",[14,558,559],{},".at",", which returns a NumPy integer rather than a Python ",[14,562,563],{},"int","; ",[14,566,567],{},"tolist()"," converts, individual lookups do not. Convert explicitly before sending: dates to ISO strings, missing values to empty strings, NumPy numbers to Python ones. A small helper keeps it in one place:",[30,570,572],{"className":68,"code":571,"language":70,"meta":35,"style":35},"def to_sheet_values(df):\n    out = df.copy()\n    for col in out.columns:\n        if pd.api.types.is_datetime64_any_dtype(out[col]):\n            out[col] = out[col].dt.strftime(\"%Y-%m-%d\")\n    out = out.astype(object).where(out.notna(), \"\")\n    return [out.columns.tolist()] + out.values.tolist()\n\nws.update(to_sheet_values(df), \"A1\", value_input_option=\"USER_ENTERED\")\nprint(_rs.calls[-1].request.body.decode())\n",[14,573,574,586,596,609,617,639,659,672,676,695],{"__ignoreMap":35},[39,575,576,579,583],{"class":41,"line":42},[39,577,578],{"class":77},"def",[39,580,582],{"class":581},"s_Opv"," to_sheet_values",[39,584,585],{"class":81},"(df):\n",[39,587,588,591,593],{"class":41,"line":85},[39,589,590],{"class":81},"    out ",[39,592,98],{"class":77},[39,594,595],{"class":81}," df.copy()\n",[39,597,598,601,604,606],{"class":41,"line":92},[39,599,600],{"class":77},"    for",[39,602,603],{"class":81}," col ",[39,605,437],{"class":77},[39,607,608],{"class":81}," out.columns:\n",[39,610,611,614],{"class":41,"line":120},[39,612,613],{"class":77},"        if",[39,615,616],{"class":81}," pd.api.types.is_datetime64_any_dtype(out[col]):\n",[39,618,619,622,624,627,630,634,637],{"class":41,"line":362},[39,620,621],{"class":81},"            out[col] ",[39,623,98],{"class":77},[39,625,626],{"class":81}," out[col].dt.strftime(",[39,628,629],{"class":49},"\"%Y-%m-",[39,631,633],{"class":632},"sSjpA","%d",[39,635,636],{"class":49},"\"",[39,638,359],{"class":81},[39,640,641,643,645,648,651,654,657],{"class":41,"line":391},[39,642,590],{"class":81},[39,644,98],{"class":77},[39,646,647],{"class":81}," out.astype(",[39,649,650],{"class":416},"object",[39,652,653],{"class":81},").where(out.notna(), ",[39,655,656],{"class":49},"\"\"",[39,658,359],{"class":81},[39,660,661,664,667,669],{"class":41,"line":423},[39,662,663],{"class":77},"    return",[39,665,666],{"class":81}," [out.columns.tolist()] ",[39,668,443],{"class":77},[39,670,671],{"class":81}," out.values.tolist()\n",[39,673,674],{"class":41,"line":428},[39,675,89],{"emptyLinePlaceholder":88},[39,677,678,681,683,685,688,690,693],{"class":41,"line":449},[39,679,680],{"class":81},"ws.update(to_sheet_values(df), ",[39,682,496],{"class":49},[39,684,382],{"class":81},[39,686,687],{"class":104},"value_input_option",[39,689,98],{"class":77},[39,691,692],{"class":49},"\"USER_ENTERED\"",[39,694,359],{"class":81},[39,696,697,700,703,706,709],{"class":41,"line":481},[39,698,699],{"class":416},"print",[39,701,702],{"class":81},"(_rs.calls[",[39,704,705],{"class":77},"-",[39,707,708],{"class":416},"1",[39,710,711],{"class":81},"].request.body.decode())\n",[10,713,714,715,718,719,722,723,726],{},"With ",[14,716,717],{},"USER_ENTERED"," the API parses ",[14,720,721],{},"2026-09-28"," the way a person typing it would, so it becomes a real date in the sheet; with the default ",[14,724,725],{},"RAW"," it stays a text string.",[25,728,730],{"id":729},"formulas-become-text-by-default","Formulas become text by default",[10,732,733,734,736,737,740,741,743],{},"That default matters even more for formulas. gspread's ",[14,735,284],{}," sends ",[14,738,739],{},"valueInputOption=RAW"," unless told otherwise, and in RAW mode a string starting with ",[14,742,98],{}," is stored as text:",[30,745,747],{"className":68,"code":746,"language":70,"meta":35,"style":35},"ws.update([[\"Total\", \"=SUM(C2:C9)\"]], \"A10\")\nprint(_rs.calls[-1].request.url.split(\"?\")[1])\nws.update([[\"Total\", \"=SUM(C2:C9)\"]], \"A10\", value_input_option=\"USER_ENTERED\")\nprint(_rs.calls[-1].request.url.split(\"?\")[1])\n",[14,748,749,770,793,817],{"__ignoreMap":35},[39,750,751,754,757,759,762,765,768],{"class":41,"line":42},[39,752,753],{"class":81},"ws.update([[",[39,755,756],{"class":49},"\"Total\"",[39,758,382],{"class":81},[39,760,761],{"class":49},"\"=SUM(C2:C9)\"",[39,763,764],{"class":81},"]], ",[39,766,767],{"class":49},"\"A10\"",[39,769,359],{"class":81},[39,771,772,774,776,778,780,783,786,789,791],{"class":41,"line":85},[39,773,699],{"class":416},[39,775,702],{"class":81},[39,777,705],{"class":77},[39,779,708],{"class":416},[39,781,782],{"class":81},"].request.url.split(",[39,784,785],{"class":49},"\"?\"",[39,787,788],{"class":81},")[",[39,790,708],{"class":416},[39,792,549],{"class":81},[39,794,795,797,799,801,803,805,807,809,811,813,815],{"class":41,"line":92},[39,796,753],{"class":81},[39,798,756],{"class":49},[39,800,382],{"class":81},[39,802,761],{"class":49},[39,804,764],{"class":81},[39,806,767],{"class":49},[39,808,382],{"class":81},[39,810,687],{"class":104},[39,812,98],{"class":77},[39,814,692],{"class":49},[39,816,359],{"class":81},[39,818,819,821,823,825,827,829,831,833,835],{"class":41,"line":120},[39,820,699],{"class":416},[39,822,702],{"class":81},[39,824,705],{"class":77},[39,826,708],{"class":416},[39,828,782],{"class":81},[39,830,785],{"class":49},[39,832,788],{"class":81},[39,834,708],{"class":416},[39,836,549],{"class":81},[10,838,839,840,843,844,846,847,851],{},"The first call puts the literal text ",[14,841,842],{},"=SUM(C2:C9)"," in the cell. In Excel files there is no such switch: openpyxl and xlsxwriter treat a string starting with ",[14,845,98],{}," as a formula, which is its own problem when the data comes from users (see ",[19,848,850],{"href":849},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-formula-injection-in-excel-exports\u002F","Prevent Formula Injection in Excel Exports",").",[146,853,155,859,155,862,155,865,155,870,155,876,155,882,155,886,155,890,155,894,155,897,155,901,155,905,155,909,155,912,155,914,155,918,155,921,155,923,155,925,155,927,155,929,155,932,155,935,155,939,155,941,155,944,155,948],{"viewBox":854,"role":149,"ariaLabelledBy":855,"xmlns":153,"style":858},"203 0 353 144",[856,857],"gs-input-t","gs-input-d","width:100%;max-width:353px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[157,860,861],{"id":856},"The same values under each input option",[161,863,864],{"id":857},"A table comparing how three sent values are stored. With RAW, the text equals SUM of C2 to C9 is stored as text, 2026-09-28 as text and 00123 as text. With USER_ENTERED, the formula is evaluated, the date becomes a date, and 00123 becomes the number 123.",[165,866],{"x":867,"y":168,"width":868,"height":869,"fill":171},"203","353","144",[165,871],{"x":872,"y":175,"width":873,"height":874,"rx":168,"fill":179,"stroke":180,"style":875},"219.8","103.1","28.0","stroke-width:1.5px",[194,877,881],{"x":878,"y":879,"style":880},"271.4","35.0","font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Sent",[165,883],{"x":884,"y":175,"width":885,"height":874,"rx":168,"fill":179,"stroke":180,"style":875},"322.9","103.7",[194,887,889],{"x":888,"y":879,"style":880},"374.7","RAW (default)",[165,891],{"x":892,"y":175,"width":893,"height":874,"rx":168,"fill":179,"stroke":180,"style":875},"426.6","113.6",[194,895,717],{"x":896,"y":879,"style":880},"483.4",[165,898],{"x":872,"y":899,"width":873,"height":874,"rx":168,"fill":171,"stroke":900,"style":205},"44.0","var(--line,#cdd5e6)",[194,902,842],{"x":878,"y":903,"style":904},"63.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[165,906],{"x":884,"y":899,"width":885,"height":874,"rx":168,"fill":907,"stroke":908,"style":875},"#fce9e9","var(--danger,#dc2626)",[194,910,194],{"x":888,"y":903,"style":911},"font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle",[165,913],{"x":892,"y":899,"width":893,"height":874,"rx":168,"fill":226,"stroke":227,"style":875},[194,915,917],{"x":896,"y":903,"style":916},"font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","formula",[165,919],{"x":872,"y":920,"width":873,"height":874,"rx":168,"fill":171,"stroke":900,"style":205},"72.0",[194,922,721],{"x":878,"y":203,"style":904},[165,924],{"x":884,"y":920,"width":885,"height":874,"rx":168,"fill":171,"stroke":900,"style":205},[194,926,194],{"x":888,"y":203,"style":904},[165,928],{"x":892,"y":920,"width":893,"height":874,"rx":168,"fill":171,"stroke":900,"style":205},[194,930,931],{"x":896,"y":203,"style":904},"date",[165,933],{"x":872,"y":934,"width":873,"height":874,"rx":168,"fill":171,"stroke":900,"style":205},"100.0",[194,936,938],{"x":878,"y":937,"style":904},"119.0","00123",[165,940],{"x":884,"y":934,"width":885,"height":874,"rx":168,"fill":171,"stroke":900,"style":205},[194,942,943],{"x":888,"y":937,"style":904},"text 00123",[165,945],{"x":892,"y":934,"width":893,"height":874,"rx":168,"fill":946,"stroke":947,"style":875},"#fdefd8","var(--gold,#b4740a)",[194,949,951],{"x":896,"y":937,"style":950},"font-size:11px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","number 123",[10,953,954,956,957,959],{},[14,955,717],{}," has its own trap, shown in the last row: values that look like numbers are parsed as numbers, so an ID such as ",[14,958,938],{}," loses its zeros. Send such columns in a separate RAW update, or format the column as plain text first.",[25,961,963],{"id":962},"reading-back-numericised-values","Reading back: numericised values",[10,965,966,967,970],{},"Reading has the mirror image of the same problem. The API returns formatted strings, and ",[14,968,969],{},"get_all_records"," converts anything that looks numeric:",[30,972,974],{"className":68,"code":973,"language":70,"meta":35,"style":35},"print(ws.get_all_records())\nprint(ws.get_all_records(numericise_ignore=[\"all\"]))\n",[14,975,976,983],{"__ignoreMap":35},[39,977,978,980],{"class":41,"line":42},[39,979,699],{"class":416},[39,981,982],{"class":81},"(ws.get_all_records())\n",[39,984,985,987,990,993,995,998,1001],{"class":41,"line":85},[39,986,699],{"class":416},[39,988,989],{"class":81},"(ws.get_all_records(",[39,991,992],{"class":104},"numericise_ignore",[39,994,98],{"class":77},[39,996,997],{"class":81},"[",[39,999,1000],{"class":49},"\"all\"",[39,1002,1003],{"class":81},"]))\n",[10,1005,1006,1008,1009,1012,1013,1008,1016,1019,1020,1023,1024,1027,1028,1031,1032,23],{},[14,1007,938],{}," becomes ",[14,1010,1011],{},"123"," and the code ",[14,1014,1015],{},"1e3",[14,1017,1018],{},"1000.0",". Pass ",[14,1021,1022],{},"numericise_ignore=[\"all\"]"," and convert the columns you know are numeric yourself, the same discipline as ",[14,1025,1026],{},"dtype=str"," with ",[14,1029,1030],{},"pd.read_excel","; see ",[19,1033,1035],{"href":1034},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fkeep-leading-zeros-when-reading-excel-with-pandas\u002F","Keep Leading Zeros When Reading Excel with pandas",[25,1037,1039],{"id":1038},"the-excel-file-route-for-comparison","The Excel file route, for comparison",[10,1041,1042],{},"The same report as a file is written locally, with types and formulas intact, and can then be emailed or uploaded to shared storage:",[30,1044,1046],{"className":68,"code":1045,"language":70,"meta":35,"style":35},"with pd.ExcelWriter(\"weekly_sales.xlsx\", engine=\"xlsxwriter\") as xw:\n    df.to_excel(xw, sheet_name=\"Report\", index=False)\n    ws_x = xw.sheets[\"Report\"]\n    ws_x.write(\"B10\", \"Total\")\n    ws_x.write_formula(\"C10\", \"=SUM(C2:C9)\")\n    ws_x.set_column(\"A:A\", 12, xw.book.add_format({\"num_format\": \"yyyy-mm-dd\"}))\nprint(pd.read_excel(\"weekly_sales.xlsx\").dtypes.astype(str).to_dict())\n",[14,1047,1048,1077,1101,1116,1130,1144,1172],{"__ignoreMap":35},[39,1049,1050,1053,1056,1059,1061,1064,1066,1069,1072,1074],{"class":41,"line":42},[39,1051,1052],{"class":77},"with",[39,1054,1055],{"class":81}," pd.ExcelWriter(",[39,1057,1058],{"class":49},"\"weekly_sales.xlsx\"",[39,1060,382],{"class":81},[39,1062,1063],{"class":104},"engine",[39,1065,98],{"class":77},[39,1067,1068],{"class":49},"\"xlsxwriter\"",[39,1070,1071],{"class":81},") ",[39,1073,319],{"class":77},[39,1075,1076],{"class":81}," xw:\n",[39,1078,1079,1082,1085,1087,1089,1091,1094,1096,1099],{"class":41,"line":85},[39,1080,1081],{"class":81},"    df.to_excel(xw, ",[39,1083,1084],{"class":104},"sheet_name",[39,1086,98],{"class":77},[39,1088,356],{"class":49},[39,1090,382],{"class":81},[39,1092,1093],{"class":104},"index",[39,1095,98],{"class":77},[39,1097,1098],{"class":416},"False",[39,1100,359],{"class":81},[39,1102,1103,1106,1108,1111,1113],{"class":41,"line":92},[39,1104,1105],{"class":81},"    ws_x ",[39,1107,98],{"class":77},[39,1109,1110],{"class":81}," xw.sheets[",[39,1112,356],{"class":49},[39,1114,1115],{"class":81},"]\n",[39,1117,1118,1121,1124,1126,1128],{"class":41,"line":120},[39,1119,1120],{"class":81},"    ws_x.write(",[39,1122,1123],{"class":49},"\"B10\"",[39,1125,382],{"class":81},[39,1127,756],{"class":49},[39,1129,359],{"class":81},[39,1131,1132,1135,1138,1140,1142],{"class":41,"line":362},[39,1133,1134],{"class":81},"    ws_x.write_formula(",[39,1136,1137],{"class":49},"\"C10\"",[39,1139,382],{"class":81},[39,1141,761],{"class":49},[39,1143,359],{"class":81},[39,1145,1146,1149,1152,1154,1157,1160,1163,1166,1169],{"class":41,"line":391},[39,1147,1148],{"class":81},"    ws_x.set_column(",[39,1150,1151],{"class":49},"\"A:A\"",[39,1153,382],{"class":81},[39,1155,1156],{"class":416},"12",[39,1158,1159],{"class":81},", xw.book.add_format({",[39,1161,1162],{"class":49},"\"num_format\"",[39,1164,1165],{"class":81},": ",[39,1167,1168],{"class":49},"\"yyyy-mm-dd\"",[39,1170,1171],{"class":81},"}))\n",[39,1173,1174,1176,1179,1181,1184,1186],{"class":41,"line":423},[39,1175,699],{"class":416},[39,1177,1178],{"class":81},"(pd.read_excel(",[39,1180,1058],{"class":49},[39,1182,1183],{"class":81},").dtypes.astype(",[39,1185,540],{"class":416},[39,1187,1188],{"class":81},").to_dict())\n",[10,1190,1191],{},"The dates come back as dates and the sales as numbers, with no conversion step, because the file format has types. The missing sales figure is an empty cell rather than an error. The only thing to remember is to keep labels such as \"Total\" out of typed columns: a text cell in the date column would make pandas read the whole column back as text.",[10,1193,1194,1195,1197],{},"Uploading the file to Google Drive is a third option: Drive can convert an ",[14,1196,16],{}," to a Google Sheet on upload, keeping most formatting, which gives Sheets users a native file without going cell by cell through the Sheets API.",[25,1199,1201],{"id":1200},"which-to-choose","Which to choose",[257,1203,1204,1210,1220],{},[260,1205,1206,1209],{},[263,1207,1208],{},"Write Excel files"," when recipients use Excel, when the report needs rich formatting or charts, when the job must run without network access or credentials, or when each run should produce an archived, immutable copy.",[260,1211,1212,1215,1216,1219],{},[263,1213,1214],{},"Update a Google Sheet"," when the audience already works in Sheets, when many people need the same live numbers at one link, when other sheets pull from it with ",[14,1217,1218],{},"IMPORTRANGE",", or when the data is small and refreshed often.",[260,1221,1222,1225,1226,1228],{},[263,1223,1224],{},"Do both"," when audiences are mixed: build the DataFrame once, write the ",[14,1227,16],{},", and push the summary table to a Sheet.",[146,1230,155,1235,155,1238,155,1241,155,1244,155,1249,155,1256,155,1261,155,1265,155,1269,155,1272,155,1277,155,1285,155,1290,155,1295,155,1300,155,1303,155,1308,155,1310,155,1313,155,1317,155,1320,155,1323,155,1328,155,1331,155,1334,155,1336,155,1339,155,1343,155,1346,155,1349,155,1356,155,1360],{"viewBox":1231,"role":149,"ariaLabelledBy":1232,"xmlns":153,"style":154},"4 0 752 272",[1233,1234],"gs-choose-t","gs-choose-d",[157,1236,1237],{"id":1233},"Where should the report go?",[161,1239,1240],{"id":1234},"A decision diagram. If recipients use Excel or the report needs charts and rich formatting, write an Excel file. If the audience works in Google Sheets and needs one live link, update a Google Sheet. If both audiences exist, write the file and push a summary to a Sheet.",[165,1242],{"x":167,"y":168,"width":169,"height":1243,"fill":171},"272",[165,1245],{"x":1246,"y":175,"width":1247,"height":1248,"rx":1156,"fill":179,"stroke":180,"style":181},"240.0","280.0","75.5",[183,1250,1252],{"transform":1251},"translate(368.0,29.0) scale(1.000)",[187,1253],{"d":1254,"fill":190,"stroke":191,"style":1255},"M9 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z M2.5 20c0-3.6 2.9-6 6.5-6s6.5 2.4 6.5 6 M16 4.5a3.3 3.3 0 0 1 0 6.3 M18 14.3c2.2.7 3.5 2.8 3.5 5.7","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[194,1257,1260],{"x":1258,"y":1248,"style":1259},"380.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Who reads the report?",[41,1262],{"x1":1258,"y1":1263,"x2":1258,"y2":1264,"stroke":180,"style":181},"91.5","111.5",[41,1266],{"x1":1267,"y1":1264,"x2":1268,"y2":1264,"stroke":180,"style":181},"134.7","625.3",[41,1270],{"x1":1267,"y1":1264,"x2":1267,"y2":1271,"stroke":180,"style":181},"157.5",[1273,1274],"polygon",{"points":1275,"fill":1276},"134.7,161.5 129.7,152.5 139.7,152.5","#5b5cf0",[165,1278],{"x":1279,"y":1280,"width":1281,"height":1282,"rx":1283,"fill":171,"stroke":900,"style":1284},"38.6","121.5","192.1","21.0","10","stroke-width:1.2px",[194,1286,1289],{"x":1267,"y":1287,"style":1288},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","Excel users or rich formatting",[165,1291],{"x":174,"y":1292,"width":1293,"height":1294,"rx":1156,"fill":179,"stroke":180,"style":181},"163.5","229.3","93.0",[183,1296,1298],{"transform":1297},"translate(122.7,176.5) scale(1.000)",[187,1299],{"d":189,"fill":190,"stroke":191,"style":1255},[194,1301,199],{"x":1267,"y":1302,"style":1259},"223.0",[194,1304,1307],{"x":1267,"y":1305,"style":1306},"242.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","openpyxl \u002F xlsxwriter",[41,1309],{"x1":1258,"y1":1264,"x2":1258,"y2":1271,"stroke":180,"style":181},[1273,1311],{"points":1312,"fill":1276},"380.0,161.5 375.0,152.5 385.0,152.5",[165,1314],{"x":1315,"y":1280,"width":1316,"height":1282,"rx":1283,"fill":171,"stroke":900,"style":1284},"332.5","95.0",[194,1318,1319],{"x":1258,"y":1287,"style":1288},"Sheets users",[165,1321],{"x":1322,"y":1292,"width":1293,"height":1294,"rx":1156,"fill":226,"stroke":227,"style":181},"265.3",[183,1324,1326],{"transform":1325},"translate(368.0,176.5) scale(1.000)",[187,1327],{"d":233,"fill":190,"stroke":234,"style":1255},[194,1329,239],{"x":1258,"y":1302,"style":1330},"font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[194,1332,1333],{"x":1258,"y":1305,"style":1306},"gspread",[41,1335],{"x1":1268,"y1":1264,"x2":1268,"y2":1271,"stroke":180,"style":181},[1273,1337],{"points":1338,"fill":1276},"625.3,161.5 620.3,152.5 630.3,152.5",[165,1340],{"x":1341,"y":1280,"width":1342,"height":1282,"rx":1283,"fill":171,"stroke":900,"style":1284},"602.9","45.0",[194,1344,1345],{"x":1268,"y":1287,"style":1288},"both",[165,1347],{"x":1348,"y":1292,"width":1293,"height":1294,"rx":1156,"fill":946,"stroke":947,"style":181},"510.7",[183,1350,1352],{"transform":1351},"translate(613.3,176.5) scale(1.000)",[187,1353],{"d":1354,"fill":190,"stroke":1355,"style":1255},"M4 12h6 M10 12l6-6h4 M10 12l6 6h4 M17 3l3 3-3 3 M17 15l3 3-3 3","var(--gold-ink,#7a4e06)",[194,1357,1359],{"x":1268,"y":1302,"style":1358},"font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","File + Sheet summary",[194,1361,1362],{"x":1268,"y":1305,"style":1306},"one DataFrame",[25,1364,1366],{"id":1365},"common-pitfalls","Common pitfalls",[257,1368,1369,1379,1387,1404,1410,1416],{},[260,1370,1371,1374,1375,1378],{},[263,1372,1373],{},"Default RAW input."," Formulas and dates arrive as text; pass ",[14,1376,1377],{},"value_input_option=\"USER_ENTERED\""," where you want them parsed.",[260,1380,1381,1384,1385,23],{},[263,1382,1383],{},"NaN and Timestamps."," The API needs plain JSON; convert dates to strings and missing values to ",[14,1386,656],{},[260,1388,1389,1392,1393,1395,1396,1398,1399,1401,1402,23],{},[263,1390,1391],{},"Numericised IDs."," ",[14,1394,969],{}," turns ",[14,1397,938],{}," into ",[14,1400,1011],{},"; use ",[14,1403,1022],{},[260,1405,1406,1409],{},[263,1407,1408],{},"Cell-by-cell updates."," Each update is a request against the quota; send whole ranges.",[260,1411,1412,1415],{},[263,1413,1414],{},"Forgetting to share with the service account."," The API returns a permission error until the sheet is shared with its email.",[260,1417,1418,1421,1422,23],{},[263,1419,1420],{},"Committed keys."," A service account JSON key is a credential; store it like a password, as in ",[19,1423,1425],{"href":1424},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fstore-credentials-for-excel-report-scripts-safely\u002F","Store Credentials for Excel Report Scripts Safely",[25,1427,1429],{"id":1428},"conclusion","Conclusion",[10,1431,1432,1433,556,1435,1437],{},"Excel files are the simpler, more capable output: no credentials, no quotas, full formatting, types preserved. Google Sheets win when the audience lives there and needs one live, shared version. If you write to Sheets with gspread, convert dates and missing values before sending, choose ",[14,1434,717],{},[14,1436,725],{}," deliberately per range, batch updates to stay within quotas, and read with numericising turned off.",[25,1439,1441],{"id":1440},"frequently-asked-questions","Frequently asked questions",[10,1443,1444,1447],{},[263,1445,1446],{},"Can Python write to Google Sheets like it writes Excel files?","\nYes, with the Sheets API; gspread is the most common library. You need a service account, the spreadsheet shared with it, and values converted to JSON-friendly types.",[10,1449,1450,1453],{},[263,1451,1452],{},"Why do my formulas show as text in Google Sheets?","\ngspread's update sends values with valueInputOption RAW by default, which stores them exactly as sent. Pass value_input_option=\"USER_ENTERED\" so the sheet parses formulas and dates.",[10,1455,1456,1459],{},[263,1457,1458],{},"How many rows can a Google Sheet hold?","\nA spreadsheet can hold up to 10 million cells across all its tabs, so the row limit depends on the number of columns. An Excel sheet holds 1,048,576 rows regardless.",[10,1461,1462,1465],{},[263,1463,1464],{},"Should a scheduled report write to Google Sheets or an Excel file?","\nWrite an Excel file when recipients use Excel or need formatting and archived copies; update a Google Sheet when the audience works in Sheets and wants one live link.",[25,1467,1469],{"id":1468},"related","Related",[257,1471,1472,1478,1485,1492,1497],{},[260,1473,1474,1475,1477],{},"Up one level: ",[19,1476,22],{"href":21}," — picking the right tool for each job.",[260,1479,1480,1484],{},[19,1481,1483],{"href":1482},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fexcel-vs-csv-vs-parquet-for-python-data-pipelines\u002F","Excel vs CSV vs Parquet for Python Data Pipelines"," — other output formats.",[260,1486,1487,1491],{},[19,1488,1490],{"href":1489},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-google-cloud-storage\u002F","Upload an Excel Report to Google Cloud Storage"," — delivering the file route.",[260,1493,1494,1496],{},[19,1495,1425],{"href":1424}," — keeping the service account key out of code.",[260,1498,1499,1501],{},[19,1500,850],{"href":849}," — the other side of strings that start with =.",[1503,1504,1505],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":35,"searchDepth":85,"depth":85,"links":1507},[1508,1509,1510,1511,1512,1513,1514,1515,1516,1517,1518],{"id":27,"depth":85,"text":28},{"id":143,"depth":85,"text":144},{"id":300,"depth":85,"text":301},{"id":729,"depth":85,"text":730},{"id":962,"depth":85,"text":963},{"id":1038,"depth":85,"text":1039},{"id":1200,"depth":85,"text":1201},{"id":1365,"depth":85,"text":1366},{"id":1428,"depth":85,"text":1429},{"id":1440,"depth":85,"text":1441},{"id":1468,"depth":85,"text":1469},"2026-09-29","Should a Python report write an .xlsx file or update a Google Sheet? A comparison of access and authentication, size limits and API quotas, formatting and formulas, how data types survive the round trip, sharing and version history, and cost — with the gspread pitfalls that turn formulas into text and IDs into numbers.","md",[1523,1525,1527,1529],{"q":1446,"a":1524},"Yes, with the Sheets API; gspread is the most common library. You need a service account, the spreadsheet shared with it, and values converted to JSON-friendly types.",{"q":1452,"a":1526},"gspread's update sends values with valueInputOption RAW by default, which stores them exactly as sent. Pass value_input_option=\"USER_ENTERED\" so the sheet parses formulas and dates.",{"q":1458,"a":1528},"A spreadsheet can hold up to 10 million cells across all its tabs, so the row limit depends on the number of columns. An Excel sheet holds 1,048,576 rows regardless.",{"q":1464,"a":1530},"Write an Excel file when recipients use Excel or need formatting and archived copies; update a Google Sheet when the audience works in Sheets and wants one live link.","google sheets vs excel files for python automation",null,{"breadcrumb":1534},[1535,1538,1541],{"name":1536,"item":1537},"Home","\u002F",{"name":1539,"item":1540},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":22,"item":21},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fgoogle-sheets-vs-excel-files-for-python-automation",{"title":1544,"description":1545},"Google Sheets vs Excel Files for Python Reports (gspread)","Compare Google Sheets (gspread) and Excel files (openpyxl, xlsxwriter) for Python automation: auth, 10 million cell limit, API quotas, RAW vs USER_ENTERED formulas, NaN and dates in JSON, numericised IDs, sharing and when to use each.","google-sheets-vs-excel-files-for-python-automation","getting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fgoogle-sheets-vs-excel-files-for-python-automation\u002Findex",[1549,1550],"choosing-libraries","publishing","comparison","_WmuREA4_B4e8C-Gcbq864c1HRMZIvhzlsowFglTKpU",[1554,1557],{"title":1483,"path":1555,"stem":1556,"children":-1},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fexcel-vs-csv-vs-parquet-for-python-data-pipelines","getting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fexcel-vs-csv-vs-parquet-for-python-data-pipelines\u002Findex",{"title":1558,"path":1559,"stem":1560,"children":-1},"Lightweight Python Excel Libraries Compared","\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Flightweight-python-excel-libraries-compared","getting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Flightweight-python-excel-libraries-compared\u002Findex",[1562,1564],{"path":1563,"title":22},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library",{"path":1565,"title":1539},"\u002Fgetting-started-with-python-excel-automation",[],[1568,1572,1575,1579,1580,1583],{"path":1569,"title":1570,"kind":1571},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fxlwings-vs-pywin32-for-excel-automation","xlwings vs pywin32 for Excel Automation","compare",{"path":1573,"title":1574,"kind":1571},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fopenpyxl-vs-xlsxwriter-vs-pandas-excelwriter","openpyxl vs xlsxwriter vs pandas.ExcelWriter",{"path":1576,"title":1577,"kind":1578},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fchoose-a-scheduler-for-python-excel-reports","Choose a Scheduler for Python Excel Reports","how-to",{"path":1559,"title":1558,"kind":1578},{"path":1581,"title":1582,"kind":1571},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fopenpyxl-vs-pandas-for-excel-automation","openpyxl vs pandas for Excel Automation",{"path":1584,"title":1585,"kind":1571},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpandas-vs-duckdb-for-excel-data","pandas vs DuckDB for Excel Data",1790972057188]