diff --git a/docs/Language Specification/bnf.txt b/docs/Language Specification/bnf.txt
index e5f082b37f9aa29909426267c078638596fad10e..c29e13874b346e24e99d1d85d1f20c01e83aa56f 100644
--- a/docs/Language Specification/bnf.txt	
+++ b/docs/Language Specification/bnf.txt	
@@ -53,10 +53,12 @@
 <factor>        -> <literal>
                 | <identifier>
                 | <call>
+                | <method_call>
                 | <list_literal>
                 | '(' <expression> ')'
 
 <call>          -> <identifier> '(' [<argument_list>] ')'
+<method_call>   -> <expression> '.' <identifier> '(' [<argument_list>] ')'
 <argument_list> -> <expression> [',' <expression>]*
 
 <list_literal>  -> '[' [<expression> [',' <expression>]*] ']'
diff --git a/examples/list.funk b/examples/list.funk
new file mode 100644
index 0000000000000000000000000000000000000000..40bdfecce926d2839304139c3fadd83a7271cc5b
--- /dev/null
+++ b/examples/list.funk
@@ -0,0 +1 @@
+print([1,2,3,4,5,6,7,8,9].length());
diff --git a/include/ast/expression/CallNode.h b/include/ast/expression/CallNode.h
index 1491c28c7ccce6c878adca71b4b7c6fb24577cd4..0cf21d0c3440c209c4290510e31454c112f78722 100644
--- a/include/ast/expression/CallNode.h
+++ b/include/ast/expression/CallNode.h
@@ -20,7 +20,7 @@ public:
 
     NodeValue get_value() const override;
 
-private:
+protected:
     Token identifier;
     Vector<ExpressionNode*> args;
 };
diff --git a/include/ast/expression/ListNode.h b/include/ast/expression/ListNode.h
new file mode 100644
index 0000000000000000000000000000000000000000..ad2e3d4ed03e84f341c45e473aeba789f25d6d00
--- /dev/null
+++ b/include/ast/expression/ListNode.h
@@ -0,0 +1,24 @@
+#pragma once
+#include "ast/expression/ExpressionNode.h"
+#include "logging/LogMacros.h"
+#include "token/Token.h"
+
+namespace funk
+{
+class ListNode : public ExpressionNode
+{
+public:
+    ListNode(const SourceLocation& location, const TokenType& type, const Vector<ExpressionNode*>& elements);
+    ~ListNode() override;
+
+    Node* evaluate() const override;
+    String to_s() const override;
+    NodeValue get_value() const override;
+
+    size_t length() const;
+
+private:
+    const TokenType type;
+    Vector<ExpressionNode*> elements;
+};
+} // namespace funk
diff --git a/include/ast/expression/MethodCallNode.h b/include/ast/expression/MethodCallNode.h
new file mode 100644
index 0000000000000000000000000000000000000000..52427452ff81c17d9837fdbbfce47106815e036c
--- /dev/null
+++ b/include/ast/expression/MethodCallNode.h
@@ -0,0 +1,21 @@
+#pragma once
+#include "ast/expression/CallNode.h"
+#include "ast/expression/ListNode.h"
+#include "logging/LogMacros.h"
+
+namespace funk
+{
+class MethodCallNode : public CallNode
+{
+public:
+    MethodCallNode(ExpressionNode* object, const Token& method, const Vector<ExpressionNode*>& args);
+    ~MethodCallNode() override;
+
+    Node* evaluate() const override;
+    String to_s() const override;
+    NodeValue get_value() const override;
+
+private:
+    ExpressionNode* object;
+};
+} // namespace funk
diff --git a/include/parser/Parser.h b/include/parser/Parser.h
index f08c405cfeb126866e9cd803818d3869a9a77534..5fc8142e5d6f407d680a4a06ac4f292c0c8e6067 100644
--- a/include/parser/Parser.h
+++ b/include/parser/Parser.h
@@ -11,9 +11,12 @@
 #include "ast/control/IfNode.h"
 #include "ast/control/WhileNode.h"
 #include "ast/declaration/DeclarationNode.h"
+#include "ast/declaration/VariableNode.h"
 #include "ast/expression/BinaryOpNode.h"
 #include "ast/expression/CallNode.h"
+#include "ast/expression/ListNode.h"
 #include "ast/expression/LiteralNode.h"
+#include "ast/expression/MethodCallNode.h"
 #include "ast/expression/UnaryOpNode.h"
 #include "lexer/Lexer.h"
 #include "logging/LogMacros.h"
@@ -213,5 +216,23 @@ private:
      * @return Node* The AST node representing the identifier
      */
     Node* parse_identifier();
+
+    /**
+     * @brief Parses a call expression
+     * @return Node* The AST node representing the call expression
+     */
+    Node* parse_call(const Token& identifier);
+
+    /**
+     * @brief Parses a method call expression
+     * @return Node* The AST node representing the method call expression
+     */
+    Node* parse_method_call(ExpressionNode* object);
+
+    /**
+     * @brief Parses a list expression
+     * @return Node* The AST node representing the list expression
+     */
+    Node* parse_list();
 };
 } // namespace funk
diff --git a/source/ast/expression/ListNode.cc b/source/ast/expression/ListNode.cc
new file mode 100644
index 0000000000000000000000000000000000000000..a09e44c35e355dc5cd0cfc5d88fd5fe8c8a3af3e
--- /dev/null
+++ b/source/ast/expression/ListNode.cc
@@ -0,0 +1,43 @@
+#include "ast/expression/ListNode.h"
+
+namespace funk
+{
+ListNode::ListNode(const SourceLocation& location, const TokenType& type, const Vector<ExpressionNode*>& elements) :
+    ExpressionNode(location), type(type), elements(elements)
+{
+}
+
+ListNode::~ListNode()
+{
+    for (Node* element : elements) { delete element; }
+}
+
+Node* ListNode::evaluate() const
+{
+    return const_cast<ListNode*>(this);
+}
+
+String ListNode::to_s() const
+{
+    String result{"[ "};
+    for (size_t i{0}; i < elements.size(); i++)
+    {
+        result += elements[i]->to_s();
+        if (i < elements.size() - 1) { result += ", "; }
+    }
+    result += " ]";
+    return result;
+}
+
+NodeValue ListNode::get_value() const
+{
+    ExpressionNode* result{dynamic_cast<ExpressionNode*>(evaluate())};
+    if (!result) { throw RuntimeError(location, "List did not evaluate to an expression"); }
+    return result->get_value();
+}
+
+size_t ListNode::length() const
+{
+    return elements.size();
+}
+} // namespace funk
diff --git a/source/ast/expression/MethodCallNode.cc b/source/ast/expression/MethodCallNode.cc
new file mode 100644
index 0000000000000000000000000000000000000000..2a4afbca0072bd07e57f83887e123a3716de05a4
--- /dev/null
+++ b/source/ast/expression/MethodCallNode.cc
@@ -0,0 +1,57 @@
+#include "ast/expression/MethodCallNode.h"
+
+namespace funk
+{
+MethodCallNode::MethodCallNode(ExpressionNode* object, const Token& method, const Vector<ExpressionNode*>& args) :
+    CallNode(method, args), object(object)
+{
+}
+
+MethodCallNode::~MethodCallNode()
+{
+    delete object;
+    CallNode::~CallNode();
+}
+
+Node* MethodCallNode::evaluate() const
+{
+    LOG_DEBUG("Evaluating method call " + identifier.get_lexeme() + " on " + object->to_s());
+
+    Node* evaluated_object{object->evaluate()};
+    if (!evaluated_object) { throw RuntimeError(location, "Failed to evaluate object for method call"); }
+
+    if (auto list_node = dynamic_cast<ListNode*>(evaluated_object))
+    {
+        if (identifier.get_lexeme() == "length")
+        {
+            return new LiteralNode(location, static_cast<int>(list_node->length()));
+        }
+    }
+
+    throw RuntimeError(
+        location, "Unknown method '" + identifier.get_lexeme() + "' for object " + evaluated_object->to_s());
+}
+
+String MethodCallNode::to_s() const
+{
+    String result{object->to_s()};
+    result += "." + identifier.get_lexeme() + "( ";
+
+    for (size_t i{0}; i < args.size(); i++)
+    {
+        result += args[i]->to_s();
+        if (i < args.size() - 1) { result += ", "; }
+    }
+
+    result += " )";
+    return result;
+}
+
+NodeValue MethodCallNode::get_value() const
+{
+    ExpressionNode* result{dynamic_cast<ExpressionNode*>(evaluate())};
+    if (!result) { throw RuntimeError(location, "Method call did not evaluate to an expression"); }
+
+    return result->get_value();
+}
+} // namespace funk
diff --git a/source/parser/Parser.cc b/source/parser/Parser.cc
index 6cb9c26acfd130b967412016effbabe4f420de98..eb66ad07773bfd2a15ee1526ae67accf0a8d60c6 100644
--- a/source/parser/Parser.cc
+++ b/source/parser/Parser.cc
@@ -288,28 +288,25 @@ Node* Parser::parse_unary()
 Node* Parser::parse_factor()
 {
     LOG_DEBUG("Parse factor");
-    if (check(TokenType::IDENTIFIER)) { return parse_identifier(); }
+    Node* expr = nullptr;
+
+    if (check(TokenType::IDENTIFIER)) { expr = parse_identifier(); }
     else if (check(TokenType::NUMB) || check(TokenType::REAL) || check(TokenType::BOOL) || check(TokenType::CHAR) ||
              check(TokenType::TEXT))
     {
-        return parse_literal();
+        expr = parse_literal();
     }
     else if (match(TokenType::L_PAR))
     {
-        Node* expr{parse_expression()};
-
-        if (!match(TokenType::R_PAR))
-        {
-            Token prev{peek_prev()};
-            SourceLocation loc{prev.get_location()};
-            SourceLocation error_loc(loc.filename, loc.line, static_cast<int>(loc.column + prev.get_lexeme().length()));
-            throw SyntaxError(error_loc, "Expected ')'");
-        }
-
-        return expr;
+        expr = parse_expression();
+        if (!match(TokenType::R_PAR)) { throw SyntaxError(peek().get_location(), "Expected ')'"); }
     }
+    else if (check(TokenType::L_BRACKET)) { expr = parse_list(); }
+    else { throw SyntaxError(peek().get_location(), "Expected expression, got " + token_type_to_s(peek().get_type())); }
+
+    while (match(TokenType::DOT)) { expr = parse_method_call(dynamic_cast<ExpressionNode*>(expr)); }
 
-    throw SyntaxError(peek_prev().get_location(), "Expected expression, got " + token_type_to_s(peek().get_type()));
+    return expr;
 }
 
 Node* Parser::parse_literal()
@@ -324,20 +321,74 @@ Node* Parser::parse_identifier()
     LOG_DEBUG("Parse identifier");
     Token identifier{next()};
 
-    if (match(TokenType::L_PAR))
+    if (check(TokenType::L_PAR)) { return parse_call(identifier); }
+    else if (match(TokenType::DOT))
     {
-        Vector<ExpressionNode*> arguments{};
-        if (!check(TokenType::R_PAR))
-        {
-            do {
-                arguments.push_back(dynamic_cast<ExpressionNode*>(parse_expression()));
-            } while (match(TokenType::COMMA));
-        }
-        if (!match(TokenType::R_PAR)) { throw SyntaxError(peek().get_location(), "Expected ')'"); }
-        return new CallNode(identifier, arguments);
+        return parse_method_call(
+            new VariableNode(identifier.get_location(), identifier.get_lexeme(), false, TokenType::NONE, nullptr));
     }
 
-    return nullptr;
+    return new VariableNode(identifier.get_location(), identifier.get_lexeme(), false, TokenType::NONE, nullptr);
+}
+
+Node* Parser::parse_call(const Token& identifier)
+{
+    LOG_DEBUG("Parse call");
+
+    if (!match(TokenType::L_PAR)) { throw SyntaxError(peek().get_location(), "Expected '('"); }
+
+    Vector<ExpressionNode*> arguments{};
+    if (!check(TokenType::R_PAR))
+    {
+        do {
+            arguments.push_back(dynamic_cast<ExpressionNode*>(parse_expression()));
+        } while (match(TokenType::COMMA));
+    }
+    if (!match(TokenType::R_PAR)) { throw SyntaxError(peek().get_location(), "Expected ')'"); }
+    return new CallNode(identifier, arguments);
+}
+
+Node* Parser::parse_method_call(ExpressionNode* object)
+{
+    LOG_DEBUG("Parse method call");
+    if (!check(TokenType::IDENTIFIER)) { throw SyntaxError(peek().get_location(), "Expected method name after '.'"); }
+
+    Token method{next()};
+
+    if (!match(TokenType::L_PAR)) { throw SyntaxError(peek().get_location(), "Expected '(' after method name"); }
+
+    Vector<ExpressionNode*> arguments{};
+    if (!check(TokenType::R_PAR))
+    {
+        do {
+            arguments.push_back(dynamic_cast<ExpressionNode*>(parse_expression()));
+        } while (match(TokenType::COMMA));
+    }
+
+    if (!match(TokenType::R_PAR)) { throw SyntaxError(peek().get_location(), "Expected ')' after method arguments"); }
+
+    return new MethodCallNode(object, method, arguments);
+}
+
+Node* Parser::parse_list()
+{
+    LOG_DEBUG("Parse list");
+    if (!match(TokenType::L_BRACKET)) { throw SyntaxError(peek().get_location(), "Expected '['"); }
+
+    Vector<ExpressionNode*> elements{};
+    TokenType type{TokenType::NONE};
+
+    if (!check(TokenType::R_BRACKET))
+    {
+        do {
+            if (type == TokenType::NONE) { type = peek().get_type(); }
+            else if (type != peek().get_type()) { throw SyntaxError(peek().get_location(), "Inconsistent list types"); }
+            elements.push_back(dynamic_cast<ExpressionNode*>(parse_expression()));
+        } while (match(TokenType::COMMA));
+    }
+
+    if (!match(TokenType::R_BRACKET)) { throw SyntaxError(peek().get_location(), "Expected ']'"); }
+    return new ListNode(peek_prev().get_location(), type, elements);
 }
 
 } // namespace funk