diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for dataframe-hasktorch
+
+## 0.1.0.0
+
+* Export `toTensor` function that converts a dataframe to a tensor.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2025 Michael Chavinda
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/dataframe-hasktorch.cabal b/dataframe-hasktorch.cabal
new file mode 100644
--- /dev/null
+++ b/dataframe-hasktorch.cabal
@@ -0,0 +1,43 @@
+cabal-version:      3.0
+name:               dataframe-hasktorch
+version:            0.1.0.0
+synopsis:           Converts between dataframes and hasktorch tensors
+
+description:
+
+license:            MIT
+license-file:       LICENSE
+
+author:             Michael Chavinda
+maintainer:         mschavinda@gmail.com
+
+category:           Data
+build-type:         Simple
+
+extra-doc-files:    CHANGELOG.md
+
+common warnings
+    ghc-options: -Wall
+
+library
+    import:           warnings
+    exposed-modules:  DataFrame.Hasktorch
+
+    build-depends:    base >= 4.11 && < 5,
+                      vector ^>= 0.13,
+                      dataframe >= 0.3.3.1 && < 0.6,
+                      hasktorch >= 0.2.1.6 && < 0.3
+
+    hs-source-dirs:   src
+
+    default-language: Haskell2010
+
+test-suite dataframe-hasktorch-test
+    import:           warnings
+    default-language: Haskell2010
+    type:             exitcode-stdio-1.0
+    hs-source-dirs:   test
+    main-is:          Main.hs
+    build-depends:
+        base >= 4.11 && < 5,
+        dataframe-hasktorch
diff --git a/src/DataFrame/Hasktorch.hs b/src/DataFrame/Hasktorch.hs
new file mode 100644
--- /dev/null
+++ b/src/DataFrame/Hasktorch.hs
@@ -0,0 +1,41 @@
+{-# LANGUAGE BangPatterns #-}
+
+module DataFrame.Hasktorch (
+    toTensor,
+) where
+
+import qualified Data.Vector as V
+import qualified Data.Vector.Unboxed as VU
+import qualified Data.Vector.Unboxed.Mutable as VUM
+import qualified DataFrame as D
+
+import Control.Exception (throw)
+import DataFrame (DataFrame)
+import Torch
+
+toTensor :: DataFrame -> Tensor
+toTensor df = case D.toMatrix df of
+    Left e -> throw e
+    Right m ->
+        let
+            (r, c) = D.dimensions df
+            dims' = if c == 1 then [r] else [r, c]
+         in
+            reshape dims' (asTensor (flattenFeatures m))
+
+flattenFeatures :: V.Vector (VU.Vector Float) -> VU.Vector Float
+flattenFeatures rows =
+    let
+        total = V.foldl' (\s v -> s + VU.length v) 0 rows
+     in
+        VU.create $ do
+            ret <- VUM.unsafeNew total
+            let go !i !off
+                    | i == V.length rows = pure ()
+                    | otherwise = do
+                        let v = rows V.! i
+                            len = VU.length v
+                        VU.unsafeCopy (VUM.unsafeSlice off len ret) v
+                        go (i + 1) (off + len)
+            go 0 0
+            pure ret
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,4 @@
+module Main (main) where
+
+main :: IO ()
+main = putStrLn "Test suite not yet implemented."
