diff --git a/python/cable_cell_io.cpp b/python/cable_cell_io.cpp
index 5191c47b730b96466e9d7f7c7b38060071eab1d7..335371df227ae9d80235a006bc5a9e742612a6aa 100644
--- a/python/cable_cell_io.cpp
+++ b/python/cable_cell_io.cpp
@@ -2,6 +2,7 @@
 #include <pybind11/stl.h>
 
 #include <fstream>
+#include <iostream>
 #include <iomanip>
 
 #include <arbor/cable_cell.hpp>
@@ -19,14 +20,10 @@ namespace pyarb {
 namespace py = pybind11;
 
 arborio::cable_cell_component load_component(py::object fn) {
-    const auto fname = util::to_path(fn);
-    std::ifstream fid{fname};
-    if (!fid.good()) {
-        throw arb::file_not_found_error(fname);
-    }
-    auto component = arborio::parse_component(fid);
+    auto contents = util::read_file_or_buffer(fn);
+    auto component = arborio::parse_component(contents);
     if (!component) {
-        throw pyarb_error("Error while trying to load component from \"" + fname + "\": " + component.error().what());
+        throw pyarb_error(std::string{"Error while trying to load component: "} + component.error().what());
     }
     return component.value();
 };
diff --git a/python/morphology.cpp b/python/morphology.cpp
index 8f4293ca13dba4128ee1f66e9ff5fc32c3ec8f54..ba238754a000fc6227f73ecb67eae1b502f99d05 100644
--- a/python/morphology.cpp
+++ b/python/morphology.cpp
@@ -252,22 +252,17 @@ void register_morphology(py::module& m) {
     // Wraps calls to C++ functions arborio::parse_swc() and arborio::load_swc_arbor().
     m.def("load_swc_arbor",
         [](py::object fn) {
-            const auto fname = util::to_path(fn);
-            std::ifstream fid{fname};
-            if (!fid.good()) {
-                throw arb::file_not_found_error(fname);
-            }
             try {
-                auto data = arborio::parse_swc(fid);
-                check_trailing(fid, fname);
+                auto contents = util::read_file_or_buffer(fn);
+                auto data = arborio::parse_swc(contents);
                 return arborio::load_swc_arbor(data);
             }
             catch (arborio::swc_error& e) {
                 // Try to produce helpful error messages for SWC parsing errors.
-                throw pyarb_error(util::pprintf("error parsing {}: {}", fname, e.what()));
+                throw pyarb_error(util::pprintf("Arbor SWC: parse error: {}", e.what()));
             }
         },
-        "filename"_a,
+        "filename_or_stream"_a,
         "Generate a morphology from an SWC file following the rules prescribed by Arbor.\n"
         "Specifically:\n"
         "* Single-segment somas are disallowed.\n"
@@ -278,23 +273,18 @@ void register_morphology(py::module& m) {
 
     m.def("load_swc_neuron",
         [](py::object fn) {
-            const auto fname = util::to_path(fn);
-            std::ifstream fid{fname};
-            if (!fid.good()) {
-                throw arb::file_not_found_error(fname);
-            }
             try {
-                auto data = arborio::parse_swc(fid);
-                check_trailing(fid, fname);
+                auto contents = util::read_file_or_buffer(fn);
+                auto data = arborio::parse_swc(contents);
                 return arborio::load_swc_neuron(data);
             }
             catch (arborio::swc_error& e) {
                 // Try to produce helpful error messages for SWC parsing errors.
                 throw pyarb_error(
-                    util::pprintf("NEURON SWC: error parsing {}: {}", fname, e.what()));
+                    util::pprintf("NEURON SWC: parse error: {}", e.what()));
             }
         },
-        "filename"_a,
+        "filename_or_stream"_a,
         "Generate a morphology from an SWC file following the rules prescribed by NEURON.\n"
         "See the documentation https://docs.arbor-sim.org/en/latest/fileformat/swc.html\n"
         "for a detailed description of the interpretation.");
@@ -343,9 +333,10 @@ void register_morphology(py::module& m) {
             "The four canonical regions are labeled 'soma', 'axon', 'dend' and 'apic'.");
 
     m.def("load_asc",
-        [](std::string fname) {
+        [](py::object fn) {
             try {
-                return arborio::load_asc(fname);
+                auto contents = util::read_file_or_buffer(fn);
+                return arborio::load_asc(contents);
             }
             catch (std::exception& e) {
                 // Try to produce helpful error messages for SWC parsing errors.
@@ -387,21 +378,16 @@ void register_morphology(py::module& m) {
         // constructors
         .def(py::init(
             [](py::object fn) {
-                const auto fname = util::to_path(fn);
-                std::ifstream fid{fname};
-                if (!fid.good()) {
-                    throw arb::file_not_found_error(fname);
-                }
                 try {
-                    std::string string_data((std::istreambuf_iterator<char>(fid)),
-                                             std::istreambuf_iterator<char>());
-                    return arborio::neuroml(string_data);
+                    auto contents = util::read_file_or_buffer(fn);
+                    return arborio::neuroml(contents);
                 }
                 catch (arborio::neuroml_exception& e) {
                     // Try to produce helpful error messages for NeuroML parsing errors.
-                    throw pyarb_error(util::pprintf("NeuroML error processing file {}: {}", fname, e.what()));
+                    throw pyarb_error(util::pprintf("NeuroML error: {}", e.what()));
                 }
-            }))
+            }),
+            "Construct NML morphology from filename or stream.")
         .def("cell_ids",
             [](const arborio::neuroml& nml) {
                 try {
diff --git a/python/test/unit/test_io.py b/python/test/unit/test_io.py
new file mode 100644
index 0000000000000000000000000000000000000000..b3eda93b99f6ca19d937c951f08882538376215a
--- /dev/null
+++ b/python/test/unit/test_io.py
@@ -0,0 +1,93 @@
+import unittest
+
+import arbor as A
+
+from pathlib import Path
+from tempfile import TemporaryDirectory as TD
+from io import StringIO
+
+acc = """(arbor-component
+  (meta-data
+    (version "0.1-dev"))
+  (cable-cell
+    (morphology
+      (branch 0 -1
+        (segment 0
+          (point -3.000000 0.000000 0.000000 3.000000)
+          (point 3.000000 0.000000 0.000000 3.000000)
+          1)))
+    (label-dict
+      (region-def "soma"
+        (tag 1))
+      (locset-def "mid"
+        (location 0 0.5)))
+    (decor
+      (default
+        (membrane-potential -40.000000))
+      (default
+        (ion-internal-concentration "ca" 0.000050))
+      (default
+        (ion-external-concentration "ca" 2.000000))
+      (default
+        (ion-reversal-potential "ca" 132.457934))
+      (default
+        (ion-internal-concentration "k" 54.400000))
+      (default
+        (ion-external-concentration "k" 2.500000))
+      (default
+        (ion-reversal-potential "k" -77.000000))
+      (default
+        (ion-internal-concentration "na" 10.000000))
+      (default
+        (ion-external-concentration "na" 140.000000))
+      (default
+        (ion-reversal-potential "na" 50.000000))
+      (paint
+        (tag 1)
+        (density
+          (mechanism "default::hh"
+            ("gnabar" 0.120000)
+            ("el" -54.300000)
+            ("q10" 0.000000)
+            ("gl" 0.000300)
+            ("gkbar" 0.036000))))
+      (place
+        (location 0 0.5)
+        (current-clamp
+          (envelope
+            (10.000000 0.800000)
+            (12.000000 0.000000))
+          0.000000 0.000000)
+        "I Clamp 0"))))
+"""
+
+
+class TestAccIo(unittest.TestCase):
+    def test_stringio(self):
+        sio = StringIO(acc)
+        A.load_component(sio)
+
+    def test_fileio(self):
+        fn = "test.acc"
+        with TD() as tmp:
+            tmp = Path(tmp)
+            with open(tmp / fn, "w") as fd:
+                fd.write(acc)
+            with open(tmp / fn) as fd:
+                A.load_component(fd)
+
+    def test_nameio(self):
+        fn = "test.acc"
+        with TD() as tmp:
+            tmp = Path(tmp)
+            with open(tmp / fn, "w") as fd:
+                fd.write(acc)
+            A.load_component(str(tmp / fn))
+
+    def test_pathio(self):
+        fn = "test.acc"
+        with TD() as tmp:
+            tmp = Path(tmp)
+            with open(tmp / fn, "w") as fd:
+                fd.write(acc)
+            A.load_component(tmp / fn)
diff --git a/python/util.hpp b/python/util.hpp
index fe1b6b07d100b7e46fe41b82029aa51282344b33..4a096f6f0a319409f97d02355b2f5f160b098fbc 100644
--- a/python/util.hpp
+++ b/python/util.hpp
@@ -1,8 +1,11 @@
 #pragma once
 
+#include <fstream>
+
 #include <pybind11/pybind11.h>
 
 #include "strprintf.hpp"
+#include "error.hpp"
 
 namespace pyarb {
 namespace util {
@@ -19,9 +22,29 @@ std::string to_path(py::object fn) {
         return std::string{py::str(fn)};
     }
     throw std::runtime_error(
-        util::strprintf("Cannot convert objects of type '{}' to a path-like.",
+        util::pprintf("Cannot convert objects of type {} to a path-like.",
                         std::string{py::str(fn.get_type())}));
 }
 
+inline
+std::string read_file_or_buffer(py::object fn) {
+    if (py::hasattr(fn, "read")) {
+        return py::str(fn.attr("read")(-1));
+    } else {
+        const auto fname = util::to_path(fn);
+        std::ifstream fid{fname};
+        if (!fid.good()) {
+            throw arb::file_not_found_error(fname);
+        }
+        std::string result;
+        fid.seekg(0, fid.end);
+        auto sz = fid.tellg();
+        fid.seekg(0, fid.beg);
+        result.resize(sz);
+        fid.read(result.data(), sz);
+        return result;
+    }
+}
+
 }
 }