[{"data":1,"prerenderedAt":2219},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fautomate-libreoffice-calc-from-python-with-uno":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fautomate-libreoffice-calc-from-python-with-uno":2185,"ancestors:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fautomate-libreoffice-calc-from-python-with-uno":2193,"children:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fautomate-libreoffice-calc-from-python-with-uno":2197,"related:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fautomate-libreoffice-calc-from-python-with-uno":2198},{"id":4,"title":5,"body":6,"dateModified":2152,"datePublished":2152,"description":2153,"extension":2154,"faq":2155,"keyword":2164,"kind":2165,"meta":2166,"navigation":190,"path":2174,"seo":2175,"slug":2178,"stem":2179,"topics":2180,"type":2183,"__hash__":2184},"docs\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fautomate-libreoffice-calc-from-python-with-uno\u002Findex.md","Automate LibreOffice Calc from Python with UNO",{"type":7,"value":8,"toc":2139},"minimark",[9,29,34,49,89,108,112,115,860,873,1049,1053,1073,1378,1393,1423,1562,1566,1575,1696,1712,1716,1719,1840,1849,1968,1972,1981,1985,1994,1998,2052,2056,2074,2078,2084,2090,2096,2102,2106,2135],[10,11,12,13,17,18,22,23,28],"p",{},"openpyxl and XlsxWriter write files; they do not ",[14,15,16],"em",{},"run"," a spreadsheet. They cannot recalculate formulas, they do not adjust references when rows are inserted or deleted, and they cannot print to PDF. On Windows, those jobs go to Excel through COM. On a Linux server, or anywhere without an Excel licence, LibreOffice can do them: it ships a full spreadsheet engine that Python can drive through UNO, LibreOffice's component interface. The ",[19,20,21],"code",{},"soffice --convert-to"," command covers one-shot conversions; UNO is for everything in between — open a workbook, change it with the engine's own rules, recalculate, save and export. This page builds a small, reusable session helper and uses it on a real task. It belongs to ",[24,25,27],"a",{"href":26},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002F","Handling Excel File Formats and Conversions",".",[30,31,33],"h2",{"id":32},"prerequisites","Prerequisites",[10,35,36,37,40,41,44,45,48],{},"LibreOffice must be installed (",[19,38,39],{},"soffice"," on the ",[19,42,43],{},"PATH","). The ",[19,46,47],{},"uno"," module that talks to it comes with LibreOffice, not from PyPI, and it is built for one specific Python:",[50,51,52,75],"ul",{},[53,54,55,59,60,63,64,67,68,71,72,28],"li",{},[56,57,58],"strong",{},"Linux:"," install your distribution's ",[19,61,62],{},"python3-uno"," package; it works with the system ",[19,65,66],{},"python3"," (",[19,69,70],{},"\u002Fusr\u002Fbin\u002Fpython3","). A virtual environment only sees it if created with ",[19,73,74],{},"python3 -m venv --system-site-packages",[53,76,77,80,81,84,85,88],{},[56,78,79],{},"Windows and macOS:"," use the Python bundled with LibreOffice (",[19,82,83],{},"program\\python.exe"," on Windows, ",[19,86,87],{},"LibreOffice.app\u002FContents\u002FResources\u002Fpython"," on macOS).",[10,90,91,92,95,96,99,100,103,104,107],{},"A quick check is ",[19,93,94],{},"python3 -c \"import uno\"","; an ",[19,97,98],{},"ImportError"," means you are running a Python that cannot see LibreOffice's module. ",[19,101,102],{},"budget.xlsx"," is a five-line budget with a ",[19,105,106],{},"SUM"," total and a share calculated from it.",[30,109,111],{"id":110},"step-1-start-soffice-and-connect","Step 1 — Start soffice and connect",[10,113,114],{},"LibreOffice runs as a separate process that listens for UNO connections. A named pipe avoids picking a TCP port, and a private profile directory keeps the job from clashing with a desktop LibreOffice or another job:",[116,117,122],"pre",{"className":118,"code":119,"language":120,"meta":121,"style":121},"language-python shiki shiki-themes github-light github-dark-high-contrast","import contextlib\nimport os\nimport shutil\nimport subprocess\nimport tempfile\nimport time\nimport uuid\n\nimport uno\nfrom com.sun.star.beans import PropertyValue\n\ndef props(**kwargs):\n    out = []\n    for name, value in kwargs.items():\n        p = PropertyValue()\n        p.Name, p.Value = name, value\n        out.append(p)\n    return tuple(out)\n\n@contextlib.contextmanager\ndef calc_session(timeout=30):\n    pipe = f\"calc_{uuid.uuid4().hex}\"\n    profile = tempfile.mkdtemp(prefix=\"lo-profile-\")\n    proc = subprocess.Popen([\n        \"soffice\", \"--headless\", \"--invisible\", \"--nologo\", \"--norestore\",\n        f\"-env:UserInstallation={uno.systemPathToFileUrl(profile)}\",\n        f\"--accept=pipe,name={pipe};urp;\",\n    ])\n    local = uno.getComponentContext()\n    resolver = local.ServiceManager.createInstanceWithContext(\"com.sun.star.bridge.UnoUrlResolver\", local)\n    deadline = time.monotonic() + timeout\n    while True:\n        try:\n            ctx = resolver.resolve(f\"uno:pipe,name={pipe};urp;StarOffice.ComponentContext\")\n            break\n        except Exception:\n            if time.monotonic() > deadline or proc.poll() is not None:\n                proc.kill()\n                raise RuntimeError(\"LibreOffice did not start\")\n            time.sleep(0.3)\n    desktop = ctx.ServiceManager.createInstanceWithContext(\"com.sun.star.frame.Desktop\", ctx)\n    try:\n        yield desktop\n    finally:\n        try:\n            desktop.terminate()\n        except Exception:\n            pass                       # the bridge drops as soffice exits\n        try:\n            proc.wait(timeout=timeout)\n        except subprocess.TimeoutExpired:\n            proc.kill()\n        shutil.rmtree(profile, ignore_errors=True)\n\ndef open_doc(desktop, path):\n    url = uno.systemPathToFileUrl(os.path.abspath(path))\n    return desktop.loadComponentFromURL(url, \"_blank\", 0, props(Hidden=True))\n\ndef save_as(doc, path, filter_name):\n    doc.storeToURL(uno.systemPathToFileUrl(os.path.abspath(path)), props(FilterName=filter_name))\n","python","",[19,123,124,137,145,153,161,169,177,185,192,200,214,219,238,250,265,276,287,293,306,311,317,336,364,387,398,428,449,469,475,486,503,520,532,540,568,574,585,616,622,638,649,666,674,683,691,698,704,713,723,730,744,752,758,774,779,790,801,830,835,846],{"__ignoreMap":121},[125,126,129,133],"span",{"class":127,"line":128},"line",1,[125,130,132],{"class":131},"s-kum","import",[125,134,136],{"class":135},"skGVy"," contextlib\n",[125,138,140,142],{"class":127,"line":139},2,[125,141,132],{"class":131},[125,143,144],{"class":135}," os\n",[125,146,148,150],{"class":127,"line":147},3,[125,149,132],{"class":131},[125,151,152],{"class":135}," shutil\n",[125,154,156,158],{"class":127,"line":155},4,[125,157,132],{"class":131},[125,159,160],{"class":135}," subprocess\n",[125,162,164,166],{"class":127,"line":163},5,[125,165,132],{"class":131},[125,167,168],{"class":135}," tempfile\n",[125,170,172,174],{"class":127,"line":171},6,[125,173,132],{"class":131},[125,175,176],{"class":135}," time\n",[125,178,180,182],{"class":127,"line":179},7,[125,181,132],{"class":131},[125,183,184],{"class":135}," uuid\n",[125,186,188],{"class":127,"line":187},8,[125,189,191],{"emptyLinePlaceholder":190},true,"\n",[125,193,195,197],{"class":127,"line":194},9,[125,196,132],{"class":131},[125,198,199],{"class":135}," uno\n",[125,201,203,206,209,211],{"class":127,"line":202},10,[125,204,205],{"class":131},"from",[125,207,208],{"class":135}," com.sun.star.beans ",[125,210,132],{"class":131},[125,212,213],{"class":135}," PropertyValue\n",[125,215,217],{"class":127,"line":216},11,[125,218,191],{"emptyLinePlaceholder":190},[125,220,222,225,229,232,235],{"class":127,"line":221},12,[125,223,224],{"class":131},"def",[125,226,228],{"class":227},"s_Opv"," props",[125,230,231],{"class":135},"(",[125,233,234],{"class":131},"**",[125,236,237],{"class":135},"kwargs):\n",[125,239,241,244,247],{"class":127,"line":240},13,[125,242,243],{"class":135},"    out ",[125,245,246],{"class":131},"=",[125,248,249],{"class":135}," []\n",[125,251,253,256,259,262],{"class":127,"line":252},14,[125,254,255],{"class":131},"    for",[125,257,258],{"class":135}," name, value ",[125,260,261],{"class":131},"in",[125,263,264],{"class":135}," kwargs.items():\n",[125,266,268,271,273],{"class":127,"line":267},15,[125,269,270],{"class":135},"        p ",[125,272,246],{"class":131},[125,274,275],{"class":135}," PropertyValue()\n",[125,277,279,282,284],{"class":127,"line":278},16,[125,280,281],{"class":135},"        p.Name, p.Value ",[125,283,246],{"class":131},[125,285,286],{"class":135}," name, value\n",[125,288,290],{"class":127,"line":289},17,[125,291,292],{"class":135},"        out.append(p)\n",[125,294,296,299,303],{"class":127,"line":295},18,[125,297,298],{"class":131},"    return",[125,300,302],{"class":301},"sP0c6"," tuple",[125,304,305],{"class":135},"(out)\n",[125,307,309],{"class":127,"line":308},19,[125,310,191],{"emptyLinePlaceholder":190},[125,312,314],{"class":127,"line":313},20,[125,315,316],{"class":227},"@contextlib.contextmanager\n",[125,318,320,322,325,328,330,333],{"class":127,"line":319},21,[125,321,224],{"class":131},[125,323,324],{"class":227}," calc_session",[125,326,327],{"class":135},"(timeout",[125,329,246],{"class":131},[125,331,332],{"class":301},"30",[125,334,335],{"class":135},"):\n",[125,337,339,342,344,347,351,355,358,361],{"class":127,"line":338},22,[125,340,341],{"class":135},"    pipe ",[125,343,246],{"class":131},[125,345,346],{"class":131}," f",[125,348,350],{"class":349},"srMev","\"calc_",[125,352,354],{"class":353},"sSjpA","{",[125,356,357],{"class":135},"uuid.uuid4().hex",[125,359,360],{"class":353},"}",[125,362,363],{"class":349},"\"\n",[125,365,367,370,372,375,379,381,384],{"class":127,"line":366},23,[125,368,369],{"class":135},"    profile ",[125,371,246],{"class":131},[125,373,374],{"class":135}," tempfile.mkdtemp(",[125,376,378],{"class":377},"sa561","prefix",[125,380,246],{"class":131},[125,382,383],{"class":349},"\"lo-profile-\"",[125,385,386],{"class":135},")\n",[125,388,390,393,395],{"class":127,"line":389},24,[125,391,392],{"class":135},"    proc ",[125,394,246],{"class":131},[125,396,397],{"class":135}," subprocess.Popen([\n",[125,399,401,404,407,410,412,415,417,420,422,425],{"class":127,"line":400},25,[125,402,403],{"class":349},"        \"soffice\"",[125,405,406],{"class":135},", ",[125,408,409],{"class":349},"\"--headless\"",[125,411,406],{"class":135},[125,413,414],{"class":349},"\"--invisible\"",[125,416,406],{"class":135},[125,418,419],{"class":349},"\"--nologo\"",[125,421,406],{"class":135},[125,423,424],{"class":349},"\"--norestore\"",[125,426,427],{"class":135},",\n",[125,429,431,434,437,439,442,444,447],{"class":127,"line":430},26,[125,432,433],{"class":131},"        f",[125,435,436],{"class":349},"\"-env:UserInstallation=",[125,438,354],{"class":353},[125,440,441],{"class":135},"uno.systemPathToFileUrl(profile)",[125,443,360],{"class":353},[125,445,446],{"class":349},"\"",[125,448,427],{"class":135},[125,450,452,454,457,459,462,464,467],{"class":127,"line":451},27,[125,453,433],{"class":131},[125,455,456],{"class":349},"\"--accept=pipe,name=",[125,458,354],{"class":353},[125,460,461],{"class":135},"pipe",[125,463,360],{"class":353},[125,465,466],{"class":349},";urp;\"",[125,468,427],{"class":135},[125,470,472],{"class":127,"line":471},28,[125,473,474],{"class":135},"    ])\n",[125,476,478,481,483],{"class":127,"line":477},29,[125,479,480],{"class":135},"    local ",[125,482,246],{"class":131},[125,484,485],{"class":135}," uno.getComponentContext()\n",[125,487,489,492,494,497,500],{"class":127,"line":488},30,[125,490,491],{"class":135},"    resolver ",[125,493,246],{"class":131},[125,495,496],{"class":135}," local.ServiceManager.createInstanceWithContext(",[125,498,499],{"class":349},"\"com.sun.star.bridge.UnoUrlResolver\"",[125,501,502],{"class":135},", local)\n",[125,504,506,509,511,514,517],{"class":127,"line":505},31,[125,507,508],{"class":135},"    deadline ",[125,510,246],{"class":131},[125,512,513],{"class":135}," time.monotonic() ",[125,515,516],{"class":131},"+",[125,518,519],{"class":135}," timeout\n",[125,521,523,526,529],{"class":127,"line":522},32,[125,524,525],{"class":131},"    while",[125,527,528],{"class":301}," True",[125,530,531],{"class":135},":\n",[125,533,535,538],{"class":127,"line":534},33,[125,536,537],{"class":131},"        try",[125,539,531],{"class":135},[125,541,543,546,548,551,554,557,559,561,563,566],{"class":127,"line":542},34,[125,544,545],{"class":135},"            ctx ",[125,547,246],{"class":131},[125,549,550],{"class":135}," resolver.resolve(",[125,552,553],{"class":131},"f",[125,555,556],{"class":349},"\"uno:pipe,name=",[125,558,354],{"class":353},[125,560,461],{"class":135},[125,562,360],{"class":353},[125,564,565],{"class":349},";urp;StarOffice.ComponentContext\"",[125,567,386],{"class":135},[125,569,571],{"class":127,"line":570},35,[125,572,573],{"class":131},"            break\n",[125,575,577,580,583],{"class":127,"line":576},36,[125,578,579],{"class":131},"        except",[125,581,582],{"class":301}," Exception",[125,584,531],{"class":135},[125,586,588,591,593,596,599,602,605,608,611,614],{"class":127,"line":587},37,[125,589,590],{"class":131},"            if",[125,592,513],{"class":135},[125,594,595],{"class":131},">",[125,597,598],{"class":135}," deadline ",[125,600,601],{"class":131},"or",[125,603,604],{"class":135}," proc.poll() ",[125,606,607],{"class":131},"is",[125,609,610],{"class":131}," not",[125,612,613],{"class":301}," None",[125,615,531],{"class":135},[125,617,619],{"class":127,"line":618},38,[125,620,621],{"class":135},"                proc.kill()\n",[125,623,625,628,631,633,636],{"class":127,"line":624},39,[125,626,627],{"class":131},"                raise",[125,629,630],{"class":301}," RuntimeError",[125,632,231],{"class":135},[125,634,635],{"class":349},"\"LibreOffice did not start\"",[125,637,386],{"class":135},[125,639,641,644,647],{"class":127,"line":640},40,[125,642,643],{"class":135},"            time.sleep(",[125,645,646],{"class":301},"0.3",[125,648,386],{"class":135},[125,650,652,655,657,660,663],{"class":127,"line":651},41,[125,653,654],{"class":135},"    desktop ",[125,656,246],{"class":131},[125,658,659],{"class":135}," ctx.ServiceManager.createInstanceWithContext(",[125,661,662],{"class":349},"\"com.sun.star.frame.Desktop\"",[125,664,665],{"class":135},", ctx)\n",[125,667,669,672],{"class":127,"line":668},42,[125,670,671],{"class":131},"    try",[125,673,531],{"class":135},[125,675,677,680],{"class":127,"line":676},43,[125,678,679],{"class":131},"        yield",[125,681,682],{"class":135}," desktop\n",[125,684,686,689],{"class":127,"line":685},44,[125,687,688],{"class":131},"    finally",[125,690,531],{"class":135},[125,692,694,696],{"class":127,"line":693},45,[125,695,537],{"class":131},[125,697,531],{"class":135},[125,699,701],{"class":127,"line":700},46,[125,702,703],{"class":135},"            desktop.terminate()\n",[125,705,707,709,711],{"class":127,"line":706},47,[125,708,579],{"class":131},[125,710,582],{"class":301},[125,712,531],{"class":135},[125,714,716,719],{"class":127,"line":715},48,[125,717,718],{"class":131},"            pass",[125,720,722],{"class":721},"s-wDw","                       # the bridge drops as soffice exits\n",[125,724,726,728],{"class":127,"line":725},49,[125,727,537],{"class":131},[125,729,531],{"class":135},[125,731,733,736,739,741],{"class":127,"line":732},50,[125,734,735],{"class":135},"            proc.wait(",[125,737,738],{"class":377},"timeout",[125,740,246],{"class":131},[125,742,743],{"class":135},"timeout)\n",[125,745,747,749],{"class":127,"line":746},51,[125,748,579],{"class":131},[125,750,751],{"class":135}," subprocess.TimeoutExpired:\n",[125,753,755],{"class":127,"line":754},52,[125,756,757],{"class":135},"            proc.kill()\n",[125,759,761,764,767,769,772],{"class":127,"line":760},53,[125,762,763],{"class":135},"        shutil.rmtree(profile, ",[125,765,766],{"class":377},"ignore_errors",[125,768,246],{"class":131},[125,770,771],{"class":301},"True",[125,773,386],{"class":135},[125,775,777],{"class":127,"line":776},54,[125,778,191],{"emptyLinePlaceholder":190},[125,780,782,784,787],{"class":127,"line":781},55,[125,783,224],{"class":131},[125,785,786],{"class":227}," open_doc",[125,788,789],{"class":135},"(desktop, path):\n",[125,791,793,796,798],{"class":127,"line":792},56,[125,794,795],{"class":135},"    url ",[125,797,246],{"class":131},[125,799,800],{"class":135}," uno.systemPathToFileUrl(os.path.abspath(path))\n",[125,802,804,806,809,812,814,817,820,823,825,827],{"class":127,"line":803},57,[125,805,298],{"class":131},[125,807,808],{"class":135}," desktop.loadComponentFromURL(url, ",[125,810,811],{"class":349},"\"_blank\"",[125,813,406],{"class":135},[125,815,816],{"class":301},"0",[125,818,819],{"class":135},", props(",[125,821,822],{"class":377},"Hidden",[125,824,246],{"class":131},[125,826,771],{"class":301},[125,828,829],{"class":135},"))\n",[125,831,833],{"class":127,"line":832},58,[125,834,191],{"emptyLinePlaceholder":190},[125,836,838,840,843],{"class":127,"line":837},59,[125,839,224],{"class":131},[125,841,842],{"class":227}," save_as",[125,844,845],{"class":135},"(doc, path, filter_name):\n",[125,847,849,852,855,857],{"class":127,"line":848},60,[125,850,851],{"class":135},"    doc.storeToURL(uno.systemPathToFileUrl(os.path.abspath(path)), props(",[125,853,854],{"class":377},"FilterName",[125,856,246],{"class":131},[125,858,859],{"class":135},"filter_name))\n",[10,861,862,863,865,866,868,869,872],{},"The helper starts ",[19,864,39],{}," headless, retries the connection until the process is ready (startup takes a second or two), and guarantees that LibreOffice is shut down — and its temporary profile removed — even if the job fails half way — a leftover ",[19,867,39],{}," process holds the profile lock and makes the next run hang. ",[19,870,871],{},"desktop.terminate()"," usually tears down the connection while the call is still in flight, so the exception it may raise is expected and ignored.",[874,875,883,884,883,888,883,892,883,899,883,909,883,916,883,921,883,926,883,929,883,932,883,937,883,939,883,941,883,946,883,951,883,959,883,964,883,968,883,971,883,976,883,981,883,985,883,988,883,993,883,998,883,1001,883,1004,883,1009,883,1014,883,1019,883,1022,883,1029,883,1034,883,1037,883,1040,883,1045],"svg",{"viewBox":876,"role":877,"ariaLabelledBy":878,"xmlns":881,"style":882},"4 0 752 388","img",[879,880],"uno-seq-t","uno-seq-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  ",[885,886,887],"title",{"id":879},"How a Python script drives LibreOffice",[889,890,891],"desc",{"id":880},"The script starts soffice, which listens on a named pipe. The script connects through the UNO bridge, asks the desktop to load the workbook, calls methods on the document such as removeByIndex and calculateAll, stores the result as xlsx and PDF, and finally terminates soffice.",[893,894],"rect",{"x":895,"y":816,"width":896,"height":897,"fill":898},"4","752","388","#ffffff",[893,900],{"x":901,"y":902,"width":903,"height":904,"rx":905,"fill":906,"stroke":907,"style":908},"20.0","16.0","180.0","48.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[910,911,915],"text",{"x":912,"y":913,"style":914},"110.0","44.5","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Python script",[893,917],{"x":918,"y":902,"width":903,"height":904,"rx":905,"fill":919,"stroke":920,"style":908},"290.0","#e7ebef","var(--line,#cdd5e6)",[910,922,925],{"x":923,"y":913,"style":924},"380.0","font-size:13px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","UNO bridge",[893,927],{"x":928,"y":902,"width":903,"height":904,"rx":905,"fill":919,"stroke":920,"style":908},"560.0",[910,930,39],{"x":931,"y":913,"style":924},"650.0",[127,933],{"x1":912,"y1":934,"x2":912,"y2":935,"stroke":920,"style":936},"64.0","372.0","stroke-width:1.5px;stroke-dasharray:5 4",[127,938],{"x1":923,"y1":934,"x2":923,"y2":935,"stroke":920,"style":936},[127,940],{"x1":931,"y1":934,"x2":931,"y2":935,"stroke":920,"style":936},[127,942],{"x1":943,"y1":944,"x2":945,"y2":944,"stroke":907,"style":908},"115.0","104.0","641.0",[947,948],"polygon",{"points":949,"fill":950},"645.0,104.0 636.0,109.0 636.0,99.0","#5b5cf0",[893,952],{"x":953,"y":954,"width":955,"height":956,"rx":957,"fill":898,"stroke":920,"style":958},"298.9","75.0","162.2","21.0","10","stroke-width:1.2px",[910,960,963],{"x":923,"y":961,"style":962},"89.5","font-size:11.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","1. start --accept=pipe,…",[127,965],{"x1":943,"y1":966,"x2":967,"y2":966,"stroke":907,"style":908},"152.0","371.0",[947,969],{"points":970,"fill":950},"375.0,152.0 366.0,157.0 366.0,147.0",[893,972],{"x":973,"y":974,"width":975,"height":956,"rx":957,"fill":898,"stroke":920,"style":958},"181.5","123.0","127.0",[910,977,980],{"x":978,"y":979,"style":962},"245.0","137.5","2. resolve the pipe",[127,982],{"x1":983,"y1":984,"x2":945,"y2":984,"stroke":907,"style":908},"385.0","200.0",[947,986],{"points":987,"fill":950},"645.0,200.0 636.0,205.0 636.0,195.0",[893,989],{"x":990,"y":991,"width":992,"height":956,"rx":957,"fill":898,"stroke":920,"style":958},"424.5","171.0","181.0",[910,994,997],{"x":995,"y":996,"style":962},"515.0","185.5","3. loadComponentFromURL",[127,999],{"x1":943,"y1":1000,"x2":945,"y2":1000,"stroke":907,"style":908},"248.0",[947,1002],{"points":1003,"fill":950},"645.0,248.0 636.0,253.0 636.0,243.0",[893,1005],{"x":1006,"y":1007,"width":1008,"height":956,"rx":957,"fill":906,"stroke":907,"style":958},"314.0","219.0","131.9",[910,1010,1013],{"x":923,"y":1011,"style":1012},"233.5","font-size:11.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","4. edit, calculateAll",[127,1015],{"x1":1016,"y1":1017,"x2":1018,"y2":1017,"stroke":907,"style":908},"645.0","296.0","119.0",[947,1020],{"points":1021,"fill":950},"115.0,296.0 124.0,291.0 124.0,301.0",[893,1023],{"x":1024,"y":1025,"width":1026,"height":956,"rx":957,"fill":1027,"stroke":1028,"style":958},"294.7","267.0","170.6","#d9f4f1","var(--teal,#0f9488)",[910,1030,1033],{"x":923,"y":1031,"style":1032},"281.5","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","5. storeToURL: xlsx + PDF",[127,1035],{"x1":943,"y1":1036,"x2":945,"y2":1036,"stroke":907,"style":908},"344.0",[947,1038],{"points":1039,"fill":950},"645.0,344.0 636.0,349.0 636.0,339.0",[893,1041],{"x":1042,"y":1043,"width":1044,"height":956,"rx":957,"fill":898,"stroke":920,"style":958},"335.1","315.0","89.7",[910,1046,1048],{"x":923,"y":1047,"style":962},"329.5","6. terminate",[30,1050,1052],{"id":1051},"step-2-delete-a-row-the-way-excel-would","Step 2 — Delete a row the way Excel would",[10,1054,1055,1056,1059,1060,1063,1064,1067,1068,1072],{},"The budget has a line that should go, \"Obsolete licence\" on row 3. With openpyxl, ",[19,1057,1058],{},"delete_rows"," moves the cells but leaves ",[19,1061,1062],{},"=SUM(B2:B5)"," untouched, so the total ends up including the empty row below — and ",[19,1065,1066],{},"=B5\u002FB6"," points at the wrong cells, as shown in ",[24,1069,1071],{"href":1070},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fdelete-empty-rows-and-columns-with-openpyxl\u002F","Delete Empty Rows and Columns with openpyxl",". LibreOffice's engine adjusts every reference, exactly as Excel does when you delete a row by hand:",[116,1074,1076],{"className":118,"code":1075,"language":120,"meta":121,"style":121},"from lo_session import calc_session, open_doc, save_as\n\nwith calc_session() as desktop:\n    doc = open_doc(desktop, \"budget.xlsx\")\n    sheet = doc.Sheets.getByName(\"Budget\")\n    total = sheet.getCellRangeByName(\"B6\")\n    print(\"before:\", total.getFormula(), \"=\", total.getValue())\n\n    for row in range(sheet.getRows().getCount()):\n        if sheet.getCellByPosition(0, row).getString() == \"Obsolete licence\":\n            sheet.getRows().removeByIndex(row, 1)       # zero-based row index, count\n            break\n\n    total = sheet.getCellRangeByName(\"B5\")\n    share = sheet.getCellRangeByName(\"B6\")\n    print(\"after: \", total.getFormula(), \"=\", total.getValue(), \"|\", share.getFormula())\n\n    sheet.getCellRangeByName(\"B4\").setValue(1200)       # travel budget raised\n    doc.calculateAll()\n    print(\"recalculated total:\", total.getValue(), \"| travel share:\", round(share.getValue(), 4))\n\n    save_as(doc, \"budget_updated.xlsx\", \"Calc Office Open XML\")\n    save_as(doc, \"budget_updated.pdf\", \"calc_pdf_Export\")\n    doc.close(True)\n",[19,1077,1078,1090,1094,1108,1123,1138,1153,1172,1176,1191,1212,1226,1230,1234,1247,1260,1282,1286,1305,1310,1336,1340,1355,1369],{"__ignoreMap":121},[125,1079,1080,1082,1085,1087],{"class":127,"line":128},[125,1081,205],{"class":131},[125,1083,1084],{"class":135}," lo_session ",[125,1086,132],{"class":131},[125,1088,1089],{"class":135}," calc_session, open_doc, save_as\n",[125,1091,1092],{"class":127,"line":139},[125,1093,191],{"emptyLinePlaceholder":190},[125,1095,1096,1099,1102,1105],{"class":127,"line":147},[125,1097,1098],{"class":131},"with",[125,1100,1101],{"class":135}," calc_session() ",[125,1103,1104],{"class":131},"as",[125,1106,1107],{"class":135}," desktop:\n",[125,1109,1110,1113,1115,1118,1121],{"class":127,"line":155},[125,1111,1112],{"class":135},"    doc ",[125,1114,246],{"class":131},[125,1116,1117],{"class":135}," open_doc(desktop, ",[125,1119,1120],{"class":349},"\"budget.xlsx\"",[125,1122,386],{"class":135},[125,1124,1125,1128,1130,1133,1136],{"class":127,"line":163},[125,1126,1127],{"class":135},"    sheet ",[125,1129,246],{"class":131},[125,1131,1132],{"class":135}," doc.Sheets.getByName(",[125,1134,1135],{"class":349},"\"Budget\"",[125,1137,386],{"class":135},[125,1139,1140,1143,1145,1148,1151],{"class":127,"line":171},[125,1141,1142],{"class":135},"    total ",[125,1144,246],{"class":131},[125,1146,1147],{"class":135}," sheet.getCellRangeByName(",[125,1149,1150],{"class":349},"\"B6\"",[125,1152,386],{"class":135},[125,1154,1155,1158,1160,1163,1166,1169],{"class":127,"line":179},[125,1156,1157],{"class":301},"    print",[125,1159,231],{"class":135},[125,1161,1162],{"class":349},"\"before:\"",[125,1164,1165],{"class":135},", total.getFormula(), ",[125,1167,1168],{"class":349},"\"=\"",[125,1170,1171],{"class":135},", total.getValue())\n",[125,1173,1174],{"class":127,"line":187},[125,1175,191],{"emptyLinePlaceholder":190},[125,1177,1178,1180,1183,1185,1188],{"class":127,"line":194},[125,1179,255],{"class":131},[125,1181,1182],{"class":135}," row ",[125,1184,261],{"class":131},[125,1186,1187],{"class":301}," range",[125,1189,1190],{"class":135},"(sheet.getRows().getCount()):\n",[125,1192,1193,1196,1199,1201,1204,1207,1210],{"class":127,"line":202},[125,1194,1195],{"class":131},"        if",[125,1197,1198],{"class":135}," sheet.getCellByPosition(",[125,1200,816],{"class":301},[125,1202,1203],{"class":135},", row).getString() ",[125,1205,1206],{"class":131},"==",[125,1208,1209],{"class":349}," \"Obsolete licence\"",[125,1211,531],{"class":135},[125,1213,1214,1217,1220,1223],{"class":127,"line":216},[125,1215,1216],{"class":135},"            sheet.getRows().removeByIndex(row, ",[125,1218,1219],{"class":301},"1",[125,1221,1222],{"class":135},")       ",[125,1224,1225],{"class":721},"# zero-based row index, count\n",[125,1227,1228],{"class":127,"line":221},[125,1229,573],{"class":131},[125,1231,1232],{"class":127,"line":240},[125,1233,191],{"emptyLinePlaceholder":190},[125,1235,1236,1238,1240,1242,1245],{"class":127,"line":252},[125,1237,1142],{"class":135},[125,1239,246],{"class":131},[125,1241,1147],{"class":135},[125,1243,1244],{"class":349},"\"B5\"",[125,1246,386],{"class":135},[125,1248,1249,1252,1254,1256,1258],{"class":127,"line":267},[125,1250,1251],{"class":135},"    share ",[125,1253,246],{"class":131},[125,1255,1147],{"class":135},[125,1257,1150],{"class":349},[125,1259,386],{"class":135},[125,1261,1262,1264,1266,1269,1271,1273,1276,1279],{"class":127,"line":278},[125,1263,1157],{"class":301},[125,1265,231],{"class":135},[125,1267,1268],{"class":349},"\"after: \"",[125,1270,1165],{"class":135},[125,1272,1168],{"class":349},[125,1274,1275],{"class":135},", total.getValue(), ",[125,1277,1278],{"class":349},"\"|\"",[125,1280,1281],{"class":135},", share.getFormula())\n",[125,1283,1284],{"class":127,"line":289},[125,1285,191],{"emptyLinePlaceholder":190},[125,1287,1288,1291,1294,1297,1300,1302],{"class":127,"line":295},[125,1289,1290],{"class":135},"    sheet.getCellRangeByName(",[125,1292,1293],{"class":349},"\"B4\"",[125,1295,1296],{"class":135},").setValue(",[125,1298,1299],{"class":301},"1200",[125,1301,1222],{"class":135},[125,1303,1304],{"class":721},"# travel budget raised\n",[125,1306,1307],{"class":127,"line":308},[125,1308,1309],{"class":135},"    doc.calculateAll()\n",[125,1311,1312,1314,1316,1319,1321,1324,1326,1329,1332,1334],{"class":127,"line":313},[125,1313,1157],{"class":301},[125,1315,231],{"class":135},[125,1317,1318],{"class":349},"\"recalculated total:\"",[125,1320,1275],{"class":135},[125,1322,1323],{"class":349},"\"| travel share:\"",[125,1325,406],{"class":135},[125,1327,1328],{"class":301},"round",[125,1330,1331],{"class":135},"(share.getValue(), ",[125,1333,895],{"class":301},[125,1335,829],{"class":135},[125,1337,1338],{"class":127,"line":319},[125,1339,191],{"emptyLinePlaceholder":190},[125,1341,1342,1345,1348,1350,1353],{"class":127,"line":338},[125,1343,1344],{"class":135},"    save_as(doc, ",[125,1346,1347],{"class":349},"\"budget_updated.xlsx\"",[125,1349,406],{"class":135},[125,1351,1352],{"class":349},"\"Calc Office Open XML\"",[125,1354,386],{"class":135},[125,1356,1357,1359,1362,1364,1367],{"class":127,"line":366},[125,1358,1344],{"class":135},[125,1360,1361],{"class":349},"\"budget_updated.pdf\"",[125,1363,406],{"class":135},[125,1365,1366],{"class":349},"\"calc_pdf_Export\"",[125,1368,386],{"class":135},[125,1370,1371,1374,1376],{"class":127,"line":389},[125,1372,1373],{"class":135},"    doc.close(",[125,1375,771],{"class":301},[125,1377,386],{"class":135},[116,1379,1383],{"className":1380,"code":1381,"language":1382,"meta":121,"style":121},"language-bash shiki shiki-themes github-light github-dark-high-contrast","\u002Fusr\u002Fbin\u002Fpython3 update_budget.py\n","bash",[19,1384,1385],{"__ignoreMap":121},[125,1386,1387,1390],{"class":127,"line":128},[125,1388,70],{"class":1389},"sMTad",[125,1391,1392],{"class":349}," update_budget.py\n",[10,1394,1395,1396,1398,1399,1402,1403,1406,1407,1410,1411,1414,1415,1418,1419,1422],{},"Before the deletion the total reads ",[19,1397,1062],{}," = 21,860. After removing row 3 the formula has become ",[19,1400,1401],{},"=SUM(B2:B4)"," and the share ",[19,1404,1405],{},"=B4\u002FB5"," — LibreOffice rewrote both references. Raising the travel budget and calling ",[19,1408,1409],{},"calculateAll()"," updates the total and the share, and the document is saved as ",[19,1412,1413],{},".xlsx"," (filter ",[19,1416,1417],{},"Calc Office Open XML",") and exported as PDF (filter ",[19,1420,1421],{},"calc_pdf_Export",") from the same open document.",[874,1424,883,1429,883,1432,883,1435,883,1438,883,1445,883,1456,883,1462,883,1468,883,1474,883,1482,883,1486,883,1491,883,1495,883,1500,883,1504,883,1510,883,1513,883,1519,883,1524,883,1528,883,1532,883,1537,883,1540,883,1545,883,1548,883,1553,883,1556],{"viewBox":1425,"role":877,"ariaLabelledBy":1426,"xmlns":881,"style":882},"4 0 752 233",[1427,1428],"uno-delete-t","uno-delete-d",[885,1430,1431],{"id":1427},"Deleting row 3 with openpyxl versus LibreOffice",[889,1433,1434],{"id":1428},"With openpyxl the cells move up but the total formula still reads SUM(B2:B5) and the share still points at B5 and B6. With LibreOffice through UNO, the total becomes SUM(B2:B4) and the share becomes B4\u002FB5, matching what Excel does.",[893,1436],{"x":895,"y":816,"width":896,"height":1437,"fill":898},"233",[893,1439],{"x":901,"y":902,"width":1440,"height":1441,"rx":1442,"fill":1443,"stroke":1444,"style":908},"351.0","201.0","14","#fce9e9","var(--danger,#dc2626)",[1446,1447,1449],"g",{"transform":1448},"translate(182.5,29.0) scale(1.083)",[1450,1451],"path",{"d":1452,"fill":1453,"stroke":1454,"style":1455},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","none","var(--accent-ink,#be185d)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[910,1457,1461],{"x":1458,"y":1459,"style":1460},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","openpyxl delete_rows",[127,1463],{"x1":1464,"y1":1465,"x2":1466,"y2":1465,"stroke":1444,"style":1467},"36.0","91.0","355.0","stroke-width:1px",[910,1469,1473],{"x":1470,"y":1471,"style":1472},"60.0","111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","total still =SUM(B2:B5)",[1446,1475,1477],{"transform":1476},"translate(38.5,99.5) scale(0.625)",[1450,1478],{"d":1479,"fill":1453,"stroke":1480,"style":1481},"M6 6l12 12 M18 6L6 18","var(--accent,#f43f8f)","stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[910,1483,1485],{"x":1470,"y":1484,"style":1472},"137.0","share still =B5\u002FB6",[1446,1487,1489],{"transform":1488},"translate(38.5,125.5) scale(0.625)",[1450,1490],{"d":1479,"fill":1453,"stroke":1480,"style":1481},[910,1492,1494],{"x":1470,"y":1493,"style":1472},"163.0","no recalculation",[1446,1496,1498],{"transform":1497},"translate(38.5,151.5) scale(0.625)",[1450,1499],{"d":1479,"fill":1453,"stroke":1480,"style":1481},[910,1501,1503],{"x":1470,"y":1502,"style":1472},"189.0","no extra software",[1446,1505,1507],{"transform":1506},"translate(38.5,177.5) scale(0.625)",[1450,1508],{"d":1509,"fill":1453,"stroke":1028,"style":1481},"M5 12.5l4.5 4.5L19 7",[893,1511],{"x":1512,"y":902,"width":1440,"height":1441,"rx":1442,"fill":1027,"stroke":1028,"style":908},"389.0",[1446,1514,1516],{"transform":1515},"translate(551.5,29.0) scale(1.083)",[1450,1517],{"d":1509,"fill":1453,"stroke":1518,"style":1455},"var(--teal-ink,#0b6157)",[910,1520,1523],{"x":1521,"y":1459,"style":1522},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","LibreOffice via UNO",[127,1525],{"x1":1526,"y1":1465,"x2":1527,"y2":1465,"stroke":1028,"style":1467},"405.0","724.0",[910,1529,1531],{"x":1530,"y":1471,"style":1472},"429.0","total becomes =SUM(B2:B4)",[1446,1533,1535],{"transform":1534},"translate(407.5,99.5) scale(0.625)",[1450,1536],{"d":1509,"fill":1453,"stroke":1028,"style":1481},[910,1538,1539],{"x":1530,"y":1484,"style":1472},"share becomes =B4\u002FB5",[1446,1541,1543],{"transform":1542},"translate(407.5,125.5) scale(0.625)",[1450,1544],{"d":1509,"fill":1453,"stroke":1028,"style":1481},[910,1546,1547],{"x":1530,"y":1493,"style":1472},"calculateAll stores real results",[1446,1549,1551],{"transform":1550},"translate(407.5,151.5) scale(0.625)",[1450,1552],{"d":1509,"fill":1453,"stroke":1028,"style":1481},[910,1554,1555],{"x":1530,"y":1502,"style":1472},"needs LibreOffice installed",[1446,1557,1559],{"transform":1558},"translate(407.5,177.5) scale(0.625)",[1450,1560],{"d":1452,"fill":1453,"stroke":1561,"style":1481},"var(--gold,#b4740a)",[30,1563,1565],{"id":1564},"step-3-check-the-saved-workbook","Step 3 — Check the saved workbook",[10,1567,1568,1569,1571,1572,1574],{},"The output is an ordinary ",[19,1570,1413],{},", so the verification can run in any Python, including a virtual environment without ",[19,1573,47],{},":",[116,1576,1578],{"className":118,"code":1577,"language":120,"meta":121,"style":121},"from openpyxl import load_workbook\n\nformulas = load_workbook(\"budget_updated.xlsx\")[\"Budget\"]\nvalues = load_workbook(\"budget_updated.xlsx\", data_only=True)[\"Budget\"]\nfor row in range(2, 7):\n    print(formulas.cell(row, 1).value, \"|\", formulas.cell(row, 2).value, \"|\", values.cell(row, 2).value)\n",[19,1579,1580,1592,1596,1616,1642,1665],{"__ignoreMap":121},[125,1581,1582,1584,1587,1589],{"class":127,"line":128},[125,1583,205],{"class":131},[125,1585,1586],{"class":135}," openpyxl ",[125,1588,132],{"class":131},[125,1590,1591],{"class":135}," load_workbook\n",[125,1593,1594],{"class":127,"line":139},[125,1595,191],{"emptyLinePlaceholder":190},[125,1597,1598,1601,1603,1606,1608,1611,1613],{"class":127,"line":147},[125,1599,1600],{"class":135},"formulas ",[125,1602,246],{"class":131},[125,1604,1605],{"class":135}," load_workbook(",[125,1607,1347],{"class":349},[125,1609,1610],{"class":135},")[",[125,1612,1135],{"class":349},[125,1614,1615],{"class":135},"]\n",[125,1617,1618,1621,1623,1625,1627,1629,1632,1634,1636,1638,1640],{"class":127,"line":155},[125,1619,1620],{"class":135},"values ",[125,1622,246],{"class":131},[125,1624,1605],{"class":135},[125,1626,1347],{"class":349},[125,1628,406],{"class":135},[125,1630,1631],{"class":377},"data_only",[125,1633,246],{"class":131},[125,1635,771],{"class":301},[125,1637,1610],{"class":135},[125,1639,1135],{"class":349},[125,1641,1615],{"class":135},[125,1643,1644,1647,1649,1651,1653,1655,1658,1660,1663],{"class":127,"line":163},[125,1645,1646],{"class":131},"for",[125,1648,1182],{"class":135},[125,1650,261],{"class":131},[125,1652,1187],{"class":301},[125,1654,231],{"class":135},[125,1656,1657],{"class":301},"2",[125,1659,406],{"class":135},[125,1661,1662],{"class":301},"7",[125,1664,335],{"class":135},[125,1666,1667,1669,1672,1674,1677,1679,1682,1684,1686,1688,1691,1693],{"class":127,"line":171},[125,1668,1157],{"class":301},[125,1670,1671],{"class":135},"(formulas.cell(row, ",[125,1673,1219],{"class":301},[125,1675,1676],{"class":135},").value, ",[125,1678,1278],{"class":349},[125,1680,1681],{"class":135},", formulas.cell(row, ",[125,1683,1657],{"class":301},[125,1685,1676],{"class":135},[125,1687,1278],{"class":349},[125,1689,1690],{"class":135},", values.cell(row, ",[125,1692,1657],{"class":301},[125,1694,1695],{"class":135},").value)\n",[10,1697,1698,1699,1701,1702,1704,1705,1707,1708,28],{},"The formulas read ",[19,1700,1401],{}," and ",[19,1703,1405],{},", and the ",[19,1706,1631],{}," load sees real cached results — 21,800 and roughly 5.5% — because LibreOffice calculated and stored them. That is the property that makes a UNO pass useful at the end of a pipeline: files written by openpyxl have no cached values, so other tools read their formulas as empty; after a LibreOffice round trip they read numbers. Recalculation on its own is covered in ",[24,1709,1711],{"href":1710},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Frecalculate-excel-formulas-without-excel-in-python\u002F","Recalculate Excel Formulas Without Excel in Python",[30,1713,1715],{"id":1714},"step-4-useful-calls-for-report-jobs","Step 4 — Useful calls for report jobs",[10,1717,1718],{},"The Calc API is large; a handful of calls cover most report automation:",[1720,1721,1722,1735],"table",{},[1723,1724,1725],"thead",{},[1726,1727,1728,1732],"tr",{},[1729,1730,1731],"th",{},"Task",[1729,1733,1734],{},"Call",[1736,1737,1738,1749,1759,1775,1791,1805,1818,1828],"tbody",{},[1726,1739,1740,1744],{},[1741,1742,1743],"td",{},"A cell by address",[1741,1745,1746],{},[19,1747,1748],{},"sheet.getCellRangeByName(\"B6\")",[1726,1750,1751,1754],{},[1741,1752,1753],{},"A cell by position (zero-based)",[1741,1755,1756],{},[19,1757,1758],{},"sheet.getCellByPosition(col, row)",[1726,1760,1761,1764],{},[1741,1762,1763],{},"Read value \u002F text \u002F formula",[1741,1765,1766,406,1769,406,1772],{},[19,1767,1768],{},"getValue()",[19,1770,1771],{},"getString()",[19,1773,1774],{},"getFormula()",[1726,1776,1777,1780],{},[1741,1778,1779],{},"Write",[1741,1781,1782,406,1785,406,1788],{},[19,1783,1784],{},"setValue(1200)",[19,1786,1787],{},"setString(\"Rent\")",[19,1789,1790],{},"setFormula(\"=SUM(B2:B4)\")",[1726,1792,1793,1796],{},[1741,1794,1795],{},"A block at once",[1741,1797,1798,1801,1802],{},[19,1799,1800],{},"sheet.getCellRangeByName(\"A1:B5\").getDataArray()"," \u002F ",[19,1803,1804],{},"setDataArray(rows)",[1726,1806,1807,1810],{},[1741,1808,1809],{},"Insert \u002F delete rows",[1741,1811,1812,406,1815],{},[19,1813,1814],{},"sheet.getRows().insertByIndex(i, n)",[19,1816,1817],{},"removeByIndex(i, n)",[1726,1819,1820,1823],{},[1741,1821,1822],{},"Recalculate",[1741,1824,1825],{},[19,1826,1827],{},"doc.calculateAll()",[1726,1829,1830,1833],{},[1741,1831,1832],{},"Save as xlsx \u002F PDF",[1741,1834,1835,1801,1838],{},[19,1836,1837],{},"storeToURL(url, FilterName=\"Calc Office Open XML\")",[19,1839,1366],{},[10,1841,1842,1701,1845,1848],{},[19,1843,1844],{},"getDataArray",[19,1846,1847],{},"setDataArray"," move a whole range in one call and are much faster than cell-by-cell access over the bridge, which pays a round trip per call. Sheet and column indexes are zero-based, unlike Excel's 1-based numbering.",[874,1850,883,1855,883,1858,883,1861,883,1864,883,1868,883,1876,883,1882,883,1886,883,1891,883,1895,883,1898,883,1901,883,1908,883,1914,883,1918,883,1921,883,1924,883,1927,883,1933,883,1937,883,1941,883,1944,883,1948,883,1951,883,1954,883,1960,883,1965],{"viewBox":1851,"role":877,"ariaLabelledBy":1852,"xmlns":881,"style":882},"4 0 752 142",[1853,1854],"uno-job-t","uno-job-d",[885,1856,1857],{"id":1853},"A LibreOffice pass at the end of a report pipeline",[889,1859,1860],{"id":1854},"pandas and openpyxl build the workbook, the UNO session opens it, applies the edits that need a real spreadsheet engine, recalculates, and saves both an xlsx with cached values and a PDF for distribution.",[893,1862],{"x":895,"y":816,"width":896,"height":1863,"fill":898},"142",[893,1865],{"x":901,"y":902,"width":1866,"height":1867,"rx":905,"fill":919,"stroke":920,"style":908},"153.0","110.5",[1446,1869,1871],{"transform":1870},"translate(84.5,29.0) scale(1.000)",[1450,1872],{"d":1873,"fill":1453,"stroke":1874,"style":1875},"M12 3c-4 0-4 1.6-4 3v2h4.2v1H6c-2 0-3 1.4-3 4s1 4 3 4h2v-2.6c0-1.6 1.3-2.9 2.9-2.9h4.2c1.3 0 2.4-1.1 2.4-2.4V6c0-1.6-1.8-3-5.5-3z M10 5.5h.01 M12 21c4 0 4-1.6 4-3v-2h-4.2v-1H18c2 0 3-1.4 3-4s-1-4-3-4h-2v2.6c0 1.6-1.3 2.9-2.9 2.9H8.9c-1.3 0-2.4 1.1-2.4 2.4V18c0 1.6 1.8 3 5.5 3z M14 18.5h.01","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[910,1877,1881],{"x":1878,"y":1879,"style":1880},"96.5","75.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Build with pandas",[910,1883,1885],{"x":1878,"y":1884,"style":1880},"93.0","\u002F openpyxl",[910,1887,1890],{"x":1878,"y":1888,"style":1889},"112.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","data and formatting",[127,1892],{"x1":1893,"y1":1894,"x2":984,"y2":1894,"stroke":907,"style":908},"178.0","71.2",[947,1896],{"points":1897,"fill":950},"204.0,71.2 195.0,76.2 195.0,66.2",[893,1899],{"x":1900,"y":902,"width":1866,"height":1867,"rx":905,"fill":906,"stroke":907,"style":908},"209.0",[1446,1902,1904],{"transform":1903},"translate(273.5,37.8) scale(1.000)",[1450,1905],{"d":1906,"fill":1453,"stroke":1907,"style":1875},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6","var(--brand-strong,#4338ca)",[910,1909,1913],{"x":1910,"y":1911,"style":1912},"285.5","84.2","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Open in Calc",[910,1915,1917],{"x":1910,"y":1916,"style":1889},"103.8","calc_session()",[127,1919],{"x1":1920,"y1":1894,"x2":1512,"y2":1894,"stroke":907,"style":908},"367.0",[947,1922],{"points":1923,"fill":950},"393.0,71.2 384.0,76.2 384.0,66.2",[893,1925],{"x":1926,"y":902,"width":1866,"height":1867,"rx":905,"fill":906,"stroke":907,"style":908},"398.0",[1446,1928,1930],{"transform":1929},"translate(462.5,29.0) scale(1.000)",[1450,1931],{"d":1932,"fill":1453,"stroke":1907,"style":1875},"M12 9a3 3 0 1 0 .01 0z M12 2.5v3 M12 18.5v3 M2.5 12h3 M18.5 12h3 M5.3 5.3l2.1 2.1 M16.6 16.6l2.1 2.1 M5.3 18.7l2.1-2.1 M16.6 7.4l2.1-2.1",[910,1934,1936],{"x":1935,"y":1879,"style":1912},"474.5","Engine edits",[910,1938,1940],{"x":1935,"y":1939,"style":1889},"95.0","insert\u002Fdelete rows,",[910,1942,1943],{"x":1935,"y":1888,"style":1889},"calculateAll",[127,1945],{"x1":1946,"y1":1894,"x2":1947,"y2":1894,"stroke":907,"style":908},"556.0","578.0",[947,1949],{"points":1950,"fill":950},"582.0,71.2 573.0,76.2 573.0,66.2",[893,1952],{"x":1953,"y":902,"width":1866,"height":1867,"rx":905,"fill":1027,"stroke":1028,"style":908},"587.0",[1446,1955,1957],{"transform":1956},"translate(651.5,37.8) scale(1.000)",[1450,1958],{"d":1959,"fill":1453,"stroke":1518,"style":1875},"M6 3h9l4 4v14H6z M15 3v4h4 M9 12h6 M9 15.5h6 M9 19h4",[910,1961,1964],{"x":1962,"y":1911,"style":1963},"663.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Save and export",[910,1966,1967],{"x":1962,"y":1916,"style":1889},"xlsx with values + PDF",[30,1969,1971],{"id":1970},"step-5-run-jobs-safely-in-parallel","Step 5 — Run jobs safely in parallel",[10,1973,1974,1975,1977,1978,1980],{},"Each ",[19,1976,1917],{}," starts its own ",[19,1979,39],{}," with its own profile directory and pipe name, so two jobs on the same machine do not collide. Without a separate profile, a second LibreOffice started with the default profile hands its work to the first instance and exits, and a script waiting for its own process gets confused. Keep sessions short — open, edit, save, close — rather than holding one LibreOffice open for hours; long-lived instances accumulate memory and a crash takes every pending document with it. On servers that run many conversions, a queue with one or two workers is more reliable than starting dozens of instances at once.",[30,1982,1984],{"id":1983},"performance-notes","Performance notes",[10,1986,1987,1988,1990,1991,1993],{},"Starting LibreOffice costs one to two seconds, and the bridge adds a round trip to every call, so the slow pattern is a session per file with thousands of single-cell calls. Batch work inside one session — open several workbooks in turn before terminating — and move data in blocks with ",[19,1989,1844],{},"\u002F",[19,1992,1847],{},". In the example above the whole run, including startup, two saves and shutdown, takes a few seconds; the spreadsheet work itself is a small fraction of that.",[30,1995,1997],{"id":1996},"common-pitfalls","Common pitfalls",[50,1999,2000,2011,2023,2036,2046],{},[53,2001,2002,2007,2008,2010],{},[56,2003,2004,28],{},[19,2005,2006],{},"ImportError: No module named 'uno'"," You are running a Python that cannot see LibreOffice's module; use the system Python with ",[19,2009,62],{},", or LibreOffice's bundled Python.",[53,2012,2013,2016,2017,2019,2020,28],{},[56,2014,2015],{},"Hanging on start."," A stale ",[19,2018,39],{}," holding the profile lock; use a fresh profile directory per session and always shut down in a ",[19,2021,2022],{},"finally",[53,2024,2025,2028,2029,2032,2033,28],{},[56,2026,2027],{},"File paths."," UNO wants ",[19,2030,2031],{},"file:\u002F\u002F"," URLs; convert with ",[19,2034,2035],{},"uno.systemPathToFileUrl(os.path.abspath(path))",[53,2037,2038,2041,2042,2045],{},[56,2039,2040],{},"Saving over the input."," ",[19,2043,2044],{},"storeToURL"," writes a copy; use a new name and keep the original until the output is checked.",[53,2047,2048,2051],{},[56,2049,2050],{},"Expecting Excel-only features."," Some newer Excel functions and Power Query connections are not supported by LibreOffice; test the workbooks you depend on.",[30,2053,2055],{"id":2054},"conclusion","Conclusion",[10,2057,2058,2059,2061,2062,2064,2065,2067,2068,2070,2071,2073],{},"When a job needs a real spreadsheet engine — reference-aware row edits, recalculation with stored results, PDF export — but Excel is not available, drive LibreOffice Calc through UNO: start a headless ",[19,2060,39],{}," on a private pipe and profile, connect, open the workbook hidden, edit it with the Calc API, call ",[19,2063,1409],{},", save with ",[19,2066,2044],{}," to ",[19,2069,1413],{}," and PDF, and always terminate the process. Run it with a Python that can import ",[19,2072,47],{},", and keep each session short.",[30,2075,2077],{"id":2076},"frequently-asked-questions","Frequently asked questions",[10,2079,2080,2083],{},[56,2081,2082],{},"How do I control LibreOffice Calc from Python?","\nStart soffice in headless mode with an --accept argument, connect with uno.getComponentContext() and a UnoUrlResolver, get the Desktop service, and open documents with loadComponentFromURL. Cells, sheets and saving are then available through the Calc API.",[10,2085,2086,2089],{},[56,2087,2088],{},"Why can't my virtual environment import uno?","\nThe uno module is installed by LibreOffice or the python3-uno package for one specific interpreter, not from PyPI. Use that interpreter, or create the virtual environment with --system-site-packages so it can see the system package.",[10,2091,2092,2095],{},[56,2093,2094],{},"Does LibreOffice update formulas when rows are deleted through UNO?","\nYes. removeByIndex on sheet.getRows() deletes rows through the spreadsheet engine, which rewrites references such as SUM(B2:B5) to SUM(B2:B4), just as Excel does. openpyxl does not.",[10,2097,2098,2101],{},[56,2099,2100],{},"How do I save an xlsx and a PDF from LibreOffice in Python?","\nCall doc.storeToURL with a file URL and a FilterName property: \"Calc Office Open XML\" for .xlsx and \"calc_pdf_Export\" for PDF.",[30,2103,2105],{"id":2104},"related","Related",[50,2107,2108,2114,2121,2128],{},[53,2109,2110,2111,2113],{},"Up one level: ",[24,2112,27],{"href":26}," — converting and transforming workbooks.",[53,2115,2116,2120],{},[24,2117,2119],{"href":2118},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-to-pdf-on-linux-with-libreoffice\u002F","Convert Excel to PDF on Linux with LibreOffice"," — the one-command conversion route.",[53,2122,2123,2127],{},[24,2124,2126],{"href":2125},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002F","Automating Excel with COM and pywin32"," — the same kind of control over Excel on Windows.",[53,2129,2130,2134],{},[24,2131,2133],{"href":2132},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Finsert-and-delete-rows-and-columns-with-openpyxl\u002F","Insert and Delete Rows and Columns with openpyxl"," — what changes without a spreadsheet engine.",[2136,2137,2138],"style",{},"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 .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}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 .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 .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}",{"title":121,"searchDepth":139,"depth":139,"links":2140},[2141,2142,2143,2144,2145,2146,2147,2148,2149,2150,2151],{"id":32,"depth":139,"text":33},{"id":110,"depth":139,"text":111},{"id":1051,"depth":139,"text":1052},{"id":1564,"depth":139,"text":1565},{"id":1714,"depth":139,"text":1715},{"id":1970,"depth":139,"text":1971},{"id":1983,"depth":139,"text":1984},{"id":1996,"depth":139,"text":1997},{"id":2054,"depth":139,"text":2055},{"id":2076,"depth":139,"text":2077},{"id":2104,"depth":139,"text":2105},"2026-10-02","Use LibreOffice as a scriptable spreadsheet engine on any server: start a headless soffice, connect through UNO from Python, open an .xlsx, edit cells, delete rows so that formulas update the way they do in Excel, recalculate, save as .xlsx and export PDF, then shut down cleanly — plus which Python can import uno and how to keep parallel jobs apart.","md",[2156,2158,2160,2162],{"q":2082,"a":2157},"Start soffice in headless mode with an --accept argument, connect with uno.getComponentContext() and a UnoUrlResolver, get the Desktop service, and open documents with loadComponentFromURL. Cells, sheets and saving are then available through the Calc API.",{"q":2088,"a":2159},"The uno module is installed by LibreOffice or the python3-uno package for one specific interpreter, not from PyPI. Use that interpreter, or create the virtual environment with --system-site-packages so it can see the system package.",{"q":2094,"a":2161},"Yes. removeByIndex on sheet.getRows() deletes rows through the spreadsheet engine, which rewrites references such as SUM(B2:B5) to SUM(B2:B4), just as Excel does. openpyxl does not.",{"q":2100,"a":2163},"Call doc.storeToURL with a file URL and a FilterName property: \"Calc Office Open XML\" for .xlsx and \"calc_pdf_Export\" for PDF.","automate libreoffice calc from python with uno",null,{"breadcrumb":2167},[2168,2170,2173],{"name":2169,"item":1990},"Home",{"name":2171,"item":2172},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":27,"item":26},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fautomate-libreoffice-calc-from-python-with-uno",{"title":2176,"description":2177},"Automate LibreOffice Calc from Python with UNO (Headless)","Drive LibreOffice Calc from Python via UNO: launch headless soffice on a pipe, connect, edit cells, delete rows with formula references updated, calculateAll, storeToURL to xlsx and PDF, clean shutdown, python3-uno and per-job profiles.","automate-libreoffice-calc-from-python-with-uno","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fautomate-libreoffice-calc-from-python-with-uno\u002Findex",[2181,2182],"file-formats","com-automation","how-to","a0oY4q3Ph6OCMiZhttY2QDn0h6hLurZjtZZrpAKdfHw",[2186,2189],{"title":27,"path":2187,"stem":2188,"children":-1},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Findex",{"title":2190,"path":2191,"stem":2192,"children":-1},"Convert an Excel Sheet to a LaTeX Table with Python","\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-an-excel-sheet-to-a-latex-table","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-an-excel-sheet-to-a-latex-table\u002Findex",[2194,2195],{"path":2187,"title":27},{"path":2196,"title":2171},"\u002Fgetting-started-with-python-excel-automation",[],[2199,2202,2205,2209,2212,2216],{"path":2200,"title":2201,"kind":2183},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fconvert-excel-files-to-parquet-with-python","Convert Excel Files to Parquet with Python",{"path":2203,"title":2204,"kind":2183},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fconvert-excel-to-csv-with-python","Convert Excel to CSV with Python",{"path":2206,"title":2207,"kind":2208},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fexcel-vs-csv-vs-parquet-for-python-data-pipelines","Excel vs CSV vs Parquet for Python Data Pipelines","compare",{"path":2210,"title":2211,"kind":2208},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpick-an-excel-engine-for-xlsx-xlsm-xls-xlsb-and-ods","Pick an Excel Engine for .xlsx, .xlsm, .xls, .xlsb and .ods",{"path":2213,"title":2214,"kind":2215},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Ffix-garbled-characters-in-excel-data","Fix Garbled Characters Like \"CafÃ©\" and \"â€™\" in Excel Data","fix",{"path":2217,"title":2218,"kind":2183},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fextract-vba-macro-code-from-an-excel-file","Extract VBA Macro Code from an Excel File",1790972059009]